Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 2160 add images subcommand #2303

60 changes: 60 additions & 0 deletions pkg/build/addimage/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addimage

import (
"runtime"

"sigs.k8s.io/kind/pkg/apis/config/defaults"
"sigs.k8s.io/kind/pkg/log"
)

// Build builds a node image using the supplied options
func Build(options ...Option) error {
// default options
ctx := &buildContext{
image: DefaultImage,
baseImage: defaults.Image,
logger: log.NoopLogger{},
arch: runtime.GOARCH,
}

// apply user options
for _, option := range options {
if err := option.apply(ctx); err != nil {
return err
}
}

// verify that we're using a supported arch
if !supportedArch(ctx.arch) {
ctx.logger.Warnf("unsupported architecture %q", ctx.arch)
}
return ctx.Build()
}

func supportedArch(arch string) bool {
curtbushko marked this conversation as resolved.
Show resolved Hide resolved
switch arch {
default:
return false
// currently we nominally support building node images for these
case "amd64":
case "arm64":
case "ppc64le":
}
return true
}
177 changes: 177 additions & 0 deletions pkg/build/addimage/buildcontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
Copyrigh. 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addimage

import (
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"time"

"sigs.k8s.io/kind/pkg/build/addimage/internal/container/docker"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
"sigs.k8s.io/kind/pkg/fs"
"sigs.k8s.io/kind/pkg/log"
)

// buildContext is used to build the kind node image, and contains
// build configuration
type buildContext struct {
// option fields
image string
baseImage string
additionalImages []string
logger log.Logger
arch string
}

// Build builds the cluster node image, the sourcedir must be set on
// the buildContext
func (c *buildContext) Build() (err error) {
return c.addImages()
}

func (c *buildContext) addImages() error {

c.logger.V(0).Info("Starting to add images to base image")
// pull images to local docker
for _, imageName := range c.additionalImages {
// continue if image already exists, no need to pull
_, err := docker.ImageID(imageName)
if err == nil {
continue
}

err = docker.Pull(c.logger, imageName, dockerBuildOsAndArch(c.arch), 3)
if err != nil {
c.logger.Errorf("Add image build Failed! Failed to pull image %v: %v", imageName, err)
}
}

// create build container
c.logger.V(0).Info("Creating build container based on " + c.baseImage)
// pull images to local docker
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't do this anymore, it's too problematic for multi-arch, as discussed in previous review comments.

we pull straight to containerd in the current version of the node image build

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also a UX issue that is not thought out here, we should probably have --pull vs no --pull.

without --pull we should use the local docker stored image or tarball, which makes a predictable experience for locally built images

with --pull + --arch we can support multi-arch builds

--arch without --pull should possibly be prohibited or throw a warning, unless the inputs are tarballs not image names.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supporting tarballs would make it easier to deal with architectures locally without a registry

containerID, err := c.createBuildContainer()
// ensure we will delete it
if containerID != "" {
defer func() {
_ = exec.Command("docker", "rm", "-f", "-v", containerID).Run()
}()
}
if err != nil {
c.logger.Errorf("Image build Failed! Failed to create build container: %v", err)
return err
}
c.logger.V(0).Info("Building in " + containerID)

// For kubernetes v1.15+ (actually 1.16 alpha versions) we may need to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not be doing this for arbitrary images we're adding, this is a quirk of component images from Kubernetes's own build process.

// drop the arch suffix from images to get the expected image
archSuffix := "-" + c.arch
fixRepository := func(repository string) string {
if strings.HasSuffix(repository, archSuffix) {
fixed := strings.TrimSuffix(repository, archSuffix)
c.logger.V(1).Info("fixed: " + repository + " -> " + fixed)
repository = fixed
}
return repository
}

// Tar up the images to make the load easier (and follow the current load pattern)
// Setup the tar path where the images will be saved
dir, err := fs.TempDir("", "images-tar")
if err != nil {
return errors.Wrap(err, "failed to create tempdir")
}
defer os.RemoveAll(dir)
imagesTarFile := filepath.Join(dir, "images.tar")
// Save the images into a tar file
c.logger.V(0).Info("Saving images into tar file at " + imagesTarFile)
err = docker.SaveImages(c.additionalImages, imagesTarFile)
if err != nil {
return err
}

// setup image importer
cmder := docker.ContainerCmder(containerID)
importer := newContainerdImporter(cmder)

f, err := os.Open(imagesTarFile)
if err != nil {
return err
}
defer f.Close()
//return importer.LoadCommand().SetStdout(os.Stdout).SetStderr(os.Stderr).SetStdin(f).Run()
// we will rewrite / correct the tags as we load the image
c.logger.V(0).Info("Importing images into build container " + containerID)
if err := exec.RunWithStdinWriter(importer.LoadCommand().SetStdout(os.Stdout).SetStderr(os.Stdout), func(w io.Writer) error {
return docker.EditArchive(f, w, fixRepository, c.arch)
}); err != nil {
return err
}

// Save the image changes to a new image
c.logger.V(0).Info("Saving new image " + c.image)
saveCmd := exec.Command(
"docker", "commit",
// we need to put this back after changing it when running the image
"--change", `ENTRYPOINT [ "/usr/local/bin/entrypoint", "/sbin/init" ]`,
containerID, c.image,
)
exec.InheritOutput(saveCmd)
if err = saveCmd.Run(); err != nil {
c.logger.Errorf("Add image build Failed! Failed to save image: %v", err)
return err
}

c.logger.V(0).Info("Add image build completed.")
return nil
}

func (c *buildContext) createBuildContainer() (id string, err error) {
// attempt to explicitly pull the image if it doesn't exist locally
// we don't care if this errors, we'll still try to run which also pulls
_ = docker.Pull(c.logger, c.baseImage, dockerBuildOsAndArch(c.arch), 4)
// this should be good enough: a specific prefix, the current unix time,
// and a little random bits in case we have multiple builds simultaneously
random := rand.New(rand.NewSource(time.Now().UnixNano())).Int31()
id = fmt.Sprintf("kind-build-%d-%d", time.Now().UTC().Unix(), random)
err = docker.Run(
c.baseImage,
[]string{
"-d", // make the client exit while the container continues to run
// run containerd so that the cri command works
"--entrypoint=/usr/local/bin/containerd",
"--name=" + id,
"--platform=" + dockerBuildOsAndArch(c.arch),
},
[]string{
"",
},
)
if err != nil {
return id, errors.Wrap(err, "failed to create build container")
}
return id, nil
}

func dockerBuildOsAndArch(arch string) string {
return "linux/" + arch
}
17 changes: 17 additions & 0 deletions pkg/build/addimage/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addimage
20 changes: 20 additions & 0 deletions pkg/build/addimage/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addimage

// DefaultImage is the default name:tag for the built image
const DefaultImage = "kindest/custom-node:latest"
38 changes: 38 additions & 0 deletions pkg/build/addimage/imageimporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addimage

import (
"sigs.k8s.io/kind/pkg/exec"
)

type containerdImporter struct {
containerCmder exec.Cmder
}

func newContainerdImporter(containerCmder exec.Cmder) *containerdImporter {
return &containerdImporter{
containerCmder: containerCmder,
}
}

func (c *containerdImporter) LoadCommand() exec.Cmd {
return c.containerCmder.Command(
// TODO: ideally we do not need this in the future. we have fixed at least one image
"ctr", "--namespace=k8s.io", "images", "import", "--all-platforms", "--no-unpack", "-",
)
}
Loading