From 14508f79b8c2444b642609b6af3fb4cb0edc3fdc Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Wed, 2 Jun 2021 09:18:09 -0400 Subject: [PATCH] Add ability to include images in custom node build This adds a new `kind build add-image` command that provides the ability to "preload" container images in a custom node image. Signed-off-by: Sean McGinnis Co-authored-by: Curt Bushko --- pkg/build/addimage/build.go | 51 +++++ pkg/build/addimage/buildcontext.go | 184 ++++++++++++++++++ .../docker/save.go => addimage/defaults.go} | 14 +- pkg/build/addimage/options.go | 82 ++++++++ pkg/build/internal/build/helpers.go | 34 ++++ .../build}/imageimporter.go | 20 +- .../internal/container/docker/archive.go | 116 +++++++++++ .../internal/container/docker/doc.go | 0 .../internal/container/docker/exec.go | 0 .../internal/container/docker/image.go | 31 ++- .../internal/container/docker/image_test.go | 0 .../internal/container/docker/pull.go | 0 .../internal/container/docker/run.go | 0 pkg/build/nodeimage/build.go | 14 +- pkg/build/nodeimage/buildcontext.go | 11 +- pkg/build/nodeimage/helpers.go | 4 - pkg/cmd/kind/build/addimage/addimage.go | 83 ++++++++ pkg/cmd/kind/build/build.go | 18 +- 18 files changed, 602 insertions(+), 60 deletions(-) create mode 100644 pkg/build/addimage/build.go create mode 100644 pkg/build/addimage/buildcontext.go rename pkg/build/{nodeimage/internal/container/docker/save.go => addimage/defaults.go} (68%) create mode 100644 pkg/build/addimage/options.go create mode 100644 pkg/build/internal/build/helpers.go rename pkg/build/{nodeimage => internal/build}/imageimporter.go (77%) rename pkg/build/{nodeimage => }/internal/container/docker/archive.go (51%) rename pkg/build/{nodeimage => }/internal/container/docker/doc.go (100%) rename pkg/build/{nodeimage => }/internal/container/docker/exec.go (100%) rename pkg/build/{nodeimage => }/internal/container/docker/image.go (74%) rename pkg/build/{nodeimage => }/internal/container/docker/image_test.go (100%) rename pkg/build/{nodeimage => }/internal/container/docker/pull.go (100%) rename pkg/build/{nodeimage => }/internal/container/docker/run.go (100%) create mode 100644 pkg/cmd/kind/build/addimage/addimage.go diff --git a/pkg/build/addimage/build.go b/pkg/build/addimage/build.go new file mode 100644 index 0000000000..3bf330d6e5 --- /dev/null +++ b/pkg/build/addimage/build.go @@ -0,0 +1,51 @@ +/* +Copyright 2024 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 implements functionality to build a node image with container images included. +package addimage + +import ( + "runtime" + + "sigs.k8s.io/kind/pkg/apis/config/defaults" + "sigs.k8s.io/kind/pkg/build/internal/build" + "sigs.k8s.io/kind/pkg/log" +) + +// Build creates a new node image by combining an existing node image with a collection +// of additional images. +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 !build.SupportedArch(ctx.arch) { + ctx.logger.Warnf("unsupported architecture %q", ctx.arch) + } + return ctx.Build() +} diff --git a/pkg/build/addimage/buildcontext.go b/pkg/build/addimage/buildcontext.go new file mode 100644 index 0000000000..679ef87ba9 --- /dev/null +++ b/pkg/build/addimage/buildcontext.go @@ -0,0 +1,184 @@ +/* +Copyrigh. 2024 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" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + + "sigs.k8s.io/kind/pkg/build/internal/build" + "sigs.k8s.io/kind/pkg/build/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" +) + +const ( + // httpProxy is the HTTP_PROXY environment variable key + httpProxy = "HTTP_PROXY" + // httpsProxy is the HTTPS_PROXY environment variable key + httpsProxy = "HTTPS_PROXY" + // noProxy is the NO_PROXY environment variable key + noProxy = "NO_PROXY" +) + +// buildContext the settings to use for rebuilding the node image. +type buildContext struct { + // option fields + image string + baseImage string + additionalImages []string + logger log.Logger + arch string + alwaysPull bool +} + +// Build rebuilds the cluster node image using the buildContext to determine +// which base image and additional images to package into a new node image. +func (c *buildContext) Build() (err error) { + c.logger.V(0).Infof("Adding %v images to base image", c.additionalImages) + + // Retrieve any necessary images + for _, imageName := range c.additionalImages { + if !c.alwaysPull { + // Check if the image already exists locally + if _, err := docker.ImageID(imageName, c.arch); err == nil { + continue + } + } + + // Pull the image for the requested architecture, which may be different than this host + err = docker.Pull(c.logger, imageName, build.DockerBuildOsAndArch(c.arch), 3) + if err != nil { + c.logger.Errorf("add image build failed, unable to pull image %q: %v", imageName, err) + return err + } + } + + c.logger.V(0).Infof("Creating build container based on %q", c.baseImage) + + containerID, err := c.createBuildContainer() + if containerID != "" { + defer func() { + _ = exec.Command("docker", "rm", "-f", "-v", containerID).Run() + }() + } + if err != nil { + c.logger.Errorf("add image build failed, unable to create build container: %v", err) + return err + } + c.logger.V(1).Infof("Building in %s", containerID) + + // 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) + + // Save the images into a tar file + imagesTarFile := filepath.Join(dir, "images.tar") + c.logger.V(1).Infof("Saving images into tar file at %q", imagesTarFile) + err = docker.SaveImages(c.additionalImages, imagesTarFile) + if err != nil { + return err + } + + // Import the images from the tarfile into our build container + cmder := docker.ContainerCmder(containerID) + importer := build.NewContainerdImporter(cmder) + + f, err := os.Open(imagesTarFile) + if err != nil { + return err + } + defer f.Close() + + c.logger.V(0).Infof("Importing images into build container %s", containerID) + if err := importer.LoadCommand().SetStdout(os.Stdout).SetStderr(os.Stderr).SetStdin(f).Run(); 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" ]`, + // remove proxy settings since they're for the building process + // and should not be carried with the built image + "--change", `ENV HTTP_PROXY="" HTTPS_PROXY="" NO_PROXY=""`, + containerID, c.image, + ) + exec.InheritOutput(saveCmd) + if err = saveCmd.Run(); err != nil { + c.logger.Errorf("add image build failed, unable to save destination 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, build.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) + runArgs := []string{ + // make the client exit while the container continues to run + "-d", + // run containerd so that the cri command works + "--entrypoint=/usr/local/bin/containerd", + "--name=" + id, + "--platform=" + build.DockerBuildOsAndArch(c.arch), + "--security-opt", "seccomp=unconfined", // ignore seccomp + } + + // Pass proxy settings from environment variables to the building container + // to make them work during the building process + for _, name := range []string{httpProxy, httpsProxy, noProxy} { + val := os.Getenv(name) + if val == "" { + val = os.Getenv(strings.ToLower(name)) + } + if val != "" { + runArgs = append(runArgs, "--env", name+"="+val) + } + } + + // Run it + err = docker.Run( + c.baseImage, + runArgs, + []string{ + "", + }, + ) + + return id, errors.Wrap(err, "failed to create build container") +} diff --git a/pkg/build/nodeimage/internal/container/docker/save.go b/pkg/build/addimage/defaults.go similarity index 68% rename from pkg/build/nodeimage/internal/container/docker/save.go rename to pkg/build/addimage/defaults.go index ded4737756..d61739a840 100644 --- a/pkg/build/nodeimage/internal/container/docker/save.go +++ b/pkg/build/addimage/defaults.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +Copyright 2024 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. @@ -14,13 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package docker +package addimage -import ( - "sigs.k8s.io/kind/pkg/exec" -) - -// Save saves image to dest, as in `docker save` -func Save(image, dest string) error { - return exec.Command("docker", "save", "-o", dest, image).Run() -} +// DefaultImage is the default name:tag for the built image +const DefaultImage = "kindest/custom-node:latest" diff --git a/pkg/build/addimage/options.go b/pkg/build/addimage/options.go new file mode 100644 index 0000000000..c1a723b2f3 --- /dev/null +++ b/pkg/build/addimage/options.go @@ -0,0 +1,82 @@ +/* +Copyright 2024 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/log" +) + +// Option is a configuration option supplied to Build +type Option interface { + apply(*buildContext) error +} + +type optionAdapter func(*buildContext) error + +func (c optionAdapter) apply(o *buildContext) error { + return c(o) +} + +// WithImage configures a build to tag the built image with `image` +func WithImage(image string) Option { + return optionAdapter(func(b *buildContext) error { + b.image = image + return nil + }) +} + +// WithBaseImage configures a build to use `image` as the base image +func WithBaseImage(image string) Option { + return optionAdapter(func(b *buildContext) error { + b.baseImage = image + return nil + }) +} + +// WithAdditionalImages configures a build to add images to the node image +func WithAdditonalImages(images []string) Option { + return optionAdapter(func(b *buildContext) error { + b.additionalImages = images + return nil + }) +} + +// WithLogger sets the logger +func WithLogger(logger log.Logger) Option { + return optionAdapter(func(b *buildContext) error { + b.logger = logger + return nil + }) +} + +// WithArch sets the architecture to build for +func WithArch(arch string) Option { + return optionAdapter(func(b *buildContext) error { + if arch != "" { + b.arch = arch + } + return nil + }) +} + +// WithPullPolicy sets whether to always pull the images +func WithPullPolicy(pull bool) Option { + return optionAdapter(func(b *buildContext) error { + b.alwaysPull = pull + return nil + }) +} diff --git a/pkg/build/internal/build/helpers.go b/pkg/build/internal/build/helpers.go new file mode 100644 index 0000000000..3f8e8e6789 --- /dev/null +++ b/pkg/build/internal/build/helpers.go @@ -0,0 +1,34 @@ +/* +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 build + +// SupportedArch checks whether the requested arch is one that is officially supportedf +// by the project. +func SupportedArch(arch string) bool { + switch arch { + default: + return false + // currently we nominally support building node images for these + case "amd64": + case "arm64": + } + return true +} + +func DockerBuildOsAndArch(arch string) string { + return "linux/" + arch +} diff --git a/pkg/build/nodeimage/imageimporter.go b/pkg/build/internal/build/imageimporter.go similarity index 77% rename from pkg/build/nodeimage/imageimporter.go rename to pkg/build/internal/build/imageimporter.go index 7839263fe7..22c19c137e 100644 --- a/pkg/build/nodeimage/imageimporter.go +++ b/pkg/build/internal/build/imageimporter.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package nodeimage +package build import ( "io" @@ -22,17 +22,17 @@ import ( "sigs.k8s.io/kind/pkg/exec" ) -type containerdImporter struct { +type ContainerdImporter struct { containerCmder exec.Cmder } -func newContainerdImporter(containerCmder exec.Cmder) *containerdImporter { - return &containerdImporter{ +func NewContainerdImporter(containerCmder exec.Cmder) *ContainerdImporter { + return &ContainerdImporter{ containerCmder: containerCmder, } } -func (c *containerdImporter) Prepare() error { +func (c *ContainerdImporter) Prepare() error { if err := c.containerCmder.Command( "bash", "-c", "nohup containerd > /dev/null 2>&1 &", ).Run(); err != nil { @@ -42,29 +42,29 @@ func (c *containerdImporter) Prepare() error { return nil } -func (c *containerdImporter) End() error { +func (c *ContainerdImporter) End() error { return c.containerCmder.Command("pkill", "containerd").Run() } -func (c *containerdImporter) Pull(image, platform string) error { +func (c *ContainerdImporter) Pull(image, platform string) error { return c.containerCmder.Command( "ctr", "--namespace=k8s.io", "content", "fetch", "--platform="+platform, image, ).SetStdout(io.Discard).SetStderr(io.Discard).Run() } -func (c *containerdImporter) LoadCommand() exec.Cmd { +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", "--label=io.cri-containerd.pinned=pinned", "--all-platforms", "--no-unpack", "--digests", "-", ) } -func (c *containerdImporter) Tag(src, target string) error { +func (c *ContainerdImporter) Tag(src, target string) error { return c.containerCmder.Command( "ctr", "--namespace=k8s.io", "images", "tag", "--force", src, target, ).Run() } -func (c *containerdImporter) ListImported() ([]string, error) { +func (c *ContainerdImporter) ListImported() ([]string, error) { return exec.OutputLines(c.containerCmder.Command("ctr", "--namespace=k8s.io", "images", "list", "-q")) } diff --git a/pkg/build/nodeimage/internal/container/docker/archive.go b/pkg/build/internal/container/docker/archive.go similarity index 51% rename from pkg/build/nodeimage/internal/container/docker/archive.go rename to pkg/build/internal/container/docker/archive.go index 0006552050..cc24ef2c97 100644 --- a/pkg/build/nodeimage/internal/container/docker/archive.go +++ b/pkg/build/internal/container/docker/archive.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "os" + "strings" "sigs.k8s.io/kind/pkg/errors" ) @@ -83,6 +84,98 @@ func GetArchiveTags(path string) ([]string, error) { return res, nil } +// EditArchiveRepositories applies edit to reader's image repositories, +// IE the repository part of repository:tag in image tags +// This supports v1 / v1.1 / v1.2 Docker Image Archives +// +// editRepositories should be a function that returns the input or an edited +// form, where the input is the image repository +// +// https://github.com/moby/moby/blob/master/image/spec/v1.md +// https://github.com/moby/moby/blob/master/image/spec/v1.1.md +// https://github.com/moby/moby/blob/master/image/spec/v1.2.md +func EditArchive(reader io.Reader, writer io.Writer, editRepositories func(string) string, architectureOverride string) error { + tarReader := tar.NewReader(reader) + tarWriter := tar.NewWriter(writer) + // iterate all entries in the tarball + for { + // read an entry + hdr, err := tarReader.Next() + if err == io.EOF { + return tarWriter.Close() + } else if err != nil { + return err + } + b, err := io.ReadAll(tarReader) + if err != nil { + return err + } + + // edit the repostories and manifests files when we find them + if hdr.Name == "repositories" { + b, err = editRepositoriesFile(b, editRepositories) + if err != nil { + return err + } + hdr.Size = int64(len(b)) + } else if hdr.Name == "manifest.json" { + b, err = editManifestRepositories(b, editRepositories) + if err != nil { + return err + } + hdr.Size = int64(len(b)) + // edit image config when we find that + } else if strings.HasSuffix(hdr.Name, ".json") { + if architectureOverride != "" { + b, err = editConfigArchitecture(b, architectureOverride) + if err != nil { + return err + } + hdr.Size = int64(len(b)) + } + } + + // write to the output tarball + if err := tarWriter.WriteHeader(hdr); err != nil { + return err + } + if len(b) > 0 { + if _, err := tarWriter.Write(b); err != nil { + return err + } + } + } +} + +/* helpers */ + +func editConfigArchitecture(raw []byte, architectureOverride string) ([]byte, error) { + var cfg map[string]interface{} + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, err + } + const architecture = "architecture" + if _, ok := cfg[architecture]; !ok { + return raw, nil + } + cfg[architecture] = architectureOverride + return json.Marshal(cfg) +} + +func editRepositoriesFile(raw []byte, editRepositories func(string) string) ([]byte, error) { + tags, err := parseRepositories(raw) + if err != nil { + return nil, err + } + + fixed := make(archiveRepositories) + for repository, tagsToRefs := range tags { + fixed[editRepositories(repository)] = tagsToRefs + } + + return json.Marshal(fixed) +} + // archiveRepositories represents repository:tag:ref // // https://github.com/moby/moby/blob/master/image/spec/v1.md @@ -115,3 +208,26 @@ func parseDockerV1Manifest(data []byte) ([]metadataEntry, error) { } return entries, nil } + +func editManifestRepositories(raw []byte, editRepositories func(string) string) ([]byte, error) { + var entries []metadataEntry + if err := json.Unmarshal(raw, &entries); err != nil { + return nil, err + } + + for i, entry := range entries { + fixed := make([]string, len(entry.RepoTags)) + for i, tag := range entry.RepoTags { + parts := strings.Split(tag, ":") + if len(parts) > 2 { + return nil, fmt.Errorf("invalid repotag: %s", entry) + } + parts[0] = editRepositories(parts[0]) + fixed[i] = strings.Join(parts, ":") + } + + entries[i].RepoTags = fixed + } + + return json.Marshal(entries) +} diff --git a/pkg/build/nodeimage/internal/container/docker/doc.go b/pkg/build/internal/container/docker/doc.go similarity index 100% rename from pkg/build/nodeimage/internal/container/docker/doc.go rename to pkg/build/internal/container/docker/doc.go diff --git a/pkg/build/nodeimage/internal/container/docker/exec.go b/pkg/build/internal/container/docker/exec.go similarity index 100% rename from pkg/build/nodeimage/internal/container/docker/exec.go rename to pkg/build/internal/container/docker/exec.go diff --git a/pkg/build/nodeimage/internal/container/docker/image.go b/pkg/build/internal/container/docker/image.go similarity index 74% rename from pkg/build/nodeimage/internal/container/docker/image.go rename to pkg/build/internal/container/docker/image.go index 767fb4dc24..7ac4d17706 100644 --- a/pkg/build/nodeimage/internal/container/docker/image.go +++ b/pkg/build/internal/container/docker/image.go @@ -20,7 +20,6 @@ import ( "fmt" "strings" - "sigs.k8s.io/kind/pkg/errors" "sigs.k8s.io/kind/pkg/exec" ) @@ -74,13 +73,33 @@ func ImageInspect(containerNameOrID, format string) ([]string, error) { } // ImageID return the Id of the container image -func ImageID(containerNameOrID string) (string, error) { - lines, err := ImageInspect(containerNameOrID, "{{ .Id }}") +func ImageID(containerNameOrID, arch string) (string, error) { + lines, err := ImageInspect(containerNameOrID, "{{ .Id }} {{ .Architecture }}") if err != nil { return "", err } - if len(lines) != 1 { - return "", errors.Errorf("Docker image ID should only be one line, got %d lines", len(lines)) + + if arch != "" { + // We only want the ID that matches the architecture we're interested in, see if we can find it + for _, line := range lines { + parts := strings.Split(line, " ") + if len(parts) == 2 && parts[1] == arch { + // Found the right image + return parts[0], nil + } + } + } else if len(lines) > 0 { + parts := strings.Split(lines[0], " ") + if len(parts) == 2 && parts[1] == arch { + return parts[0], nil + } } - return lines[0], nil + + return "", fmt.Errorf("image ID for %q %s could not be found", containerNameOrID, arch) +} + +// SaveImages saves one or more images to dest, as in `docker save` +func SaveImages(images []string, dest string) error { + commandArgs := append([]string{"save", "-o", dest}, images...) + return exec.Command("docker", commandArgs...).Run() } diff --git a/pkg/build/nodeimage/internal/container/docker/image_test.go b/pkg/build/internal/container/docker/image_test.go similarity index 100% rename from pkg/build/nodeimage/internal/container/docker/image_test.go rename to pkg/build/internal/container/docker/image_test.go diff --git a/pkg/build/nodeimage/internal/container/docker/pull.go b/pkg/build/internal/container/docker/pull.go similarity index 100% rename from pkg/build/nodeimage/internal/container/docker/pull.go rename to pkg/build/internal/container/docker/pull.go diff --git a/pkg/build/nodeimage/internal/container/docker/run.go b/pkg/build/internal/container/docker/run.go similarity index 100% rename from pkg/build/nodeimage/internal/container/docker/run.go rename to pkg/build/internal/container/docker/run.go diff --git a/pkg/build/nodeimage/build.go b/pkg/build/nodeimage/build.go index f3fc0e4442..554893f051 100644 --- a/pkg/build/nodeimage/build.go +++ b/pkg/build/nodeimage/build.go @@ -22,6 +22,7 @@ import ( "os" "runtime" + "sigs.k8s.io/kind/pkg/build/internal/build" "sigs.k8s.io/kind/pkg/build/nodeimage/internal/kube" "sigs.k8s.io/kind/pkg/errors" "sigs.k8s.io/kind/pkg/internal/version" @@ -46,7 +47,7 @@ func Build(options ...Option) error { } // verify that we're using a supported arch - if !supportedArch(ctx.arch) { + if !build.SupportedArch(ctx.arch) { ctx.logger.Warnf("unsupported architecture %q", ctx.arch) } @@ -143,14 +144,3 @@ func detectBuildType(param string) string { } return "" } - -func supportedArch(arch string) bool { - switch arch { - default: - return false - // currently we nominally support building node images for these - case "amd64": - case "arm64": - } - return true -} diff --git a/pkg/build/nodeimage/buildcontext.go b/pkg/build/nodeimage/buildcontext.go index ff41132a89..e94edf93b0 100644 --- a/pkg/build/nodeimage/buildcontext.go +++ b/pkg/build/nodeimage/buildcontext.go @@ -28,7 +28,8 @@ import ( "sigs.k8s.io/kind/pkg/exec" "sigs.k8s.io/kind/pkg/log" - "sigs.k8s.io/kind/pkg/build/nodeimage/internal/container/docker" + "sigs.k8s.io/kind/pkg/build/internal/build" + "sigs.k8s.io/kind/pkg/build/internal/container/docker" "sigs.k8s.io/kind/pkg/build/nodeimage/internal/kube" "sigs.k8s.io/kind/pkg/internal/sets" "sigs.k8s.io/kind/pkg/internal/version" @@ -253,7 +254,7 @@ func (c *buildContext) prePullImagesAndWriteManifests(bits kube.Bits, parsedVers requiredImages = append(requiredImages, defaultStorageImages...) // setup image importer - importer := newContainerdImporter(cmder) + importer := build.NewContainerdImporter(cmder) if err := importer.Prepare(); err != nil { c.logger.Errorf("Image build Failed! Failed to prepare containerd to load images %v", err) return nil, err @@ -271,7 +272,7 @@ func (c *buildContext) prePullImagesAndWriteManifests(bits kube.Bits, parsedVers image := image // https://golang.org/doc/faq#closures_and_goroutines fns = append(fns, func() error { if !builtImages.Has(image) { - if err = importer.Pull(image, dockerBuildOsAndArch(c.arch)); err != nil { + if err = importer.Pull(image, build.DockerBuildOsAndArch(c.arch)); err != nil { c.logger.Warnf("Failed to pull %s with error: %v", image, err) runE := exec.RunErrorForError(err) c.logger.Warn(string(runE.Output)) @@ -328,7 +329,7 @@ func (c *buildContext) prePullImagesAndWriteManifests(bits kube.Bits, parsedVers 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 returns error, we'll still try to run which also pulls - _ = docker.Pull(c.logger, c.baseImage, dockerBuildOsAndArch(c.arch), 4) + _ = docker.Pull(c.logger, c.baseImage, build.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() @@ -338,7 +339,7 @@ func (c *buildContext) createBuildContainer() (id string, err error) { // the container should hang forever, so we can exec in it "--entrypoint=sleep", "--name=" + id, - "--platform=" + dockerBuildOsAndArch(c.arch), + "--platform=" + build.DockerBuildOsAndArch(c.arch), "--security-opt", "seccomp=unconfined", // ignore seccomp } // pass proxy settings from environment variables to the building container diff --git a/pkg/build/nodeimage/helpers.go b/pkg/build/nodeimage/helpers.go index 4e7e3dbd71..7887027d4d 100644 --- a/pkg/build/nodeimage/helpers.go +++ b/pkg/build/nodeimage/helpers.go @@ -49,7 +49,3 @@ func findSandboxImage(config string) (string, error) { } return match[1], nil } - -func dockerBuildOsAndArch(arch string) string { - return "linux/" + arch -} diff --git a/pkg/cmd/kind/build/addimage/addimage.go b/pkg/cmd/kind/build/addimage/addimage.go new file mode 100644 index 0000000000..a6a87a4ff9 --- /dev/null +++ b/pkg/cmd/kind/build/addimage/addimage.go @@ -0,0 +1,83 @@ +/* +Copyright 2024 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 ( + "github.com/spf13/cobra" + + "sigs.k8s.io/kind/pkg/apis/config/defaults" + "sigs.k8s.io/kind/pkg/build/addimage" + "sigs.k8s.io/kind/pkg/cmd" + "sigs.k8s.io/kind/pkg/errors" + "sigs.k8s.io/kind/pkg/log" +) + +type flagpole struct { + Image string + BaseImage string + Arch string + Pull bool +} + +// NewCommand returns a new cobra.Command for adding images to the node image +func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command { + flags := &flagpole{} + cmd := &cobra.Command{ + Args: cobra.MinimumNArgs(1), + Use: "add-image [IMAGE...]", + Short: "Update node image with extra images preloaded", + Long: "Use an existing node image as a base and preload extra container images.", + RunE: func(cmd *cobra.Command, args []string) error { + return runE(logger, flags, args) + }, + } + + cmd.Flags().StringVar( + &flags.Image, "image", + addimage.DefaultImage, + "name:tag of the resulting image to be built", + ) + cmd.Flags().StringVar( + &flags.BaseImage, "base-image", + defaults.Image, + "name:tag of the base image to use for the build", + ) + cmd.Flags().StringVar( + &flags.Arch, "arch", + "", + "architecture to build for, defaults to the host architecture", + ) + cmd.Flags().BoolVar( + &flags.Pull, "pull", + false, + "pull all images, even if they already exist locally", + ) + return cmd +} + +func runE(logger log.Logger, flags *flagpole, images []string) error { + err := addimage.Build( + addimage.WithImage(flags.Image), + addimage.WithBaseImage(flags.BaseImage), + addimage.WithAdditonalImages(images), + addimage.WithLogger(logger), + addimage.WithArch(flags.Arch), + addimage.WithPullPolicy(flags.Pull), + ) + + return errors.Wrap(err, "error adding images to node image") +} diff --git a/pkg/cmd/kind/build/build.go b/pkg/cmd/kind/build/build.go index 1c62df609c..a40ff237c6 100644 --- a/pkg/cmd/kind/build/build.go +++ b/pkg/cmd/kind/build/build.go @@ -18,11 +18,10 @@ limitations under the License. package build import ( - "errors" - "github.com/spf13/cobra" "sigs.k8s.io/kind/pkg/cmd" + "sigs.k8s.io/kind/pkg/cmd/kind/build/addimage" "sigs.k8s.io/kind/pkg/cmd/kind/build/nodeimage" "sigs.k8s.io/kind/pkg/log" ) @@ -30,20 +29,13 @@ import ( // NewCommand returns a new cobra.Command for building func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command { cmd := &cobra.Command{ - Args: cobra.NoArgs, - // TODO(bentheelder): more detailed usage + Args: cobra.NoArgs, Use: "build", - Short: "Build one of [node-image]", - Long: "Build one of [node-image]", - RunE: func(cmd *cobra.Command, args []string) error { - err := cmd.Help() - if err != nil { - return err - } - return errors.New("Subcommand is required") - }, + Short: "Build a kind node image", + Long: "Build a kind node image by creating a new image from source, or combining existing images in a new image.", } // add subcommands cmd.AddCommand(nodeimage.NewCommand(logger, streams)) + cmd.AddCommand(addimage.NewCommand(logger, streams)) return cmd }