From 6e0fb3cbab757f7542ad9d15692e4b7689663753 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 14 May 2024 16:37:32 -0400 Subject: [PATCH] Add a new builder for tarballs and released artifacts Signed-off-by: Davanum Srinivas fix the arch usage Signed-off-by: Davanum Srinivas --- pkg/build/nodeimage/build.go | 53 ++++- pkg/build/nodeimage/buildcontext.go | 2 +- .../nodeimage/internal/kube/builder_remote.go | 203 ++++++++++++++++++ .../internal/kube/builder_tarball.go | 78 +++++++ pkg/build/nodeimage/options.go | 6 +- pkg/cmd/kind/build/nodeimage/nodeimage.go | 5 +- 6 files changed, 328 insertions(+), 19 deletions(-) create mode 100644 pkg/build/nodeimage/internal/kube/builder_remote.go create mode 100644 pkg/build/nodeimage/internal/kube/builder_tarball.go diff --git a/pkg/build/nodeimage/build.go b/pkg/build/nodeimage/build.go index 34d68a81d4..7ca56514be 100644 --- a/pkg/build/nodeimage/build.go +++ b/pkg/build/nodeimage/build.go @@ -17,10 +17,13 @@ limitations under the License. package nodeimage import ( + "fmt" + "os" "runtime" "sigs.k8s.io/kind/pkg/build/nodeimage/internal/kube" "sigs.k8s.io/kind/pkg/errors" + "sigs.k8s.io/kind/pkg/internal/version" "sigs.k8s.io/kind/pkg/log" ) @@ -46,21 +49,49 @@ func Build(options ...Option) error { ctx.logger.Warnf("unsupported architecture %q", ctx.arch) } - // locate sources if no kubernetes source was specified - if ctx.kubeRoot == "" { - kubeRoot, err := kube.FindSource() - if err != nil { - return errors.Wrap(err, "error finding kuberoot") + if ctx.kubeParam != "" { + kubever, err := version.ParseSemantic(ctx.kubeParam) + if err == nil { + builder, err := kube.NewRemoteBuilder(ctx.logger, "v"+kubever.String(), ctx.arch) + if err != nil { + return err + } + ctx.builder = builder + } else { + if _, err := os.Stat(ctx.kubeParam); err != nil { + ctx.logger.V(0).Infof("%s is not a valid kubernetes version", ctx.kubeParam) + return fmt.Errorf("%s is not a valid kubernetes version", ctx.kubeParam) + } } - ctx.kubeRoot = kubeRoot } - // initialize bits - builder, err := kube.NewDockerBuilder(ctx.logger, ctx.kubeRoot, ctx.arch) - if err != nil { - return err + if ctx.builder == nil && ctx.kubeParam != "" { + if info, err := os.Stat(ctx.kubeParam); err == nil && info.Mode().IsRegular() { + builder, err := kube.NewTarballBuilder(ctx.logger, ctx.kubeParam) + if err != nil { + return err + } + ctx.builder = builder + } + } + + if ctx.builder == nil { + // locate sources if no kubernetes source was specified + if ctx.kubeParam == "" { + kubeRoot, err := kube.FindSource() + if err != nil { + return errors.Wrap(err, "error finding kuberoot") + } + ctx.kubeParam = kubeRoot + } + + // initialize bits + builder, err := kube.NewDockerBuilder(ctx.logger, ctx.kubeParam, ctx.arch) + if err != nil { + return err + } + ctx.builder = builder } - ctx.builder = builder // do the actual build return ctx.Build() diff --git a/pkg/build/nodeimage/buildcontext.go b/pkg/build/nodeimage/buildcontext.go index 3a6f3a91d0..495dfa3769 100644 --- a/pkg/build/nodeimage/buildcontext.go +++ b/pkg/build/nodeimage/buildcontext.go @@ -51,7 +51,7 @@ type buildContext struct { baseImage string logger log.Logger arch string - kubeRoot string + kubeParam string // non-option fields builder kube.Builder } diff --git a/pkg/build/nodeimage/internal/kube/builder_remote.go b/pkg/build/nodeimage/internal/kube/builder_remote.go new file mode 100644 index 0000000000..8a98f8a0f7 --- /dev/null +++ b/pkg/build/nodeimage/internal/kube/builder_remote.go @@ -0,0 +1,203 @@ +/* +Copyright 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 kube + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "sigs.k8s.io/kind/pkg/log" +) + +type remoteBuilder struct { + version string + arch string + logger log.Logger +} + +var _ Builder = &remoteBuilder{} + +func NewRemoteBuilder(logger log.Logger, version, arch string) (Builder, error) { + return &remoteBuilder{ + version: version, + arch: arch, + logger: logger, + }, nil +} + +// Build implements Bits.Build +func (b *remoteBuilder) Build() (Bits, error) { + url := "https://dl.k8s.io/" + b.version + "/kubernetes-server-linux-" + b.arch + ".tar.gz" + + tmpDir, err := os.MkdirTemp(os.TempDir(), "k8s-tar-extract-") + if err != nil { + return nil, fmt.Errorf("error creating temporary directory for tar extraction: %w", err) + } + + tgzFile := filepath.Join(tmpDir, "kubernetes-"+b.version+"-server-linux-amd64.tar.gz") + err = b.downloadURL(url, tgzFile) + if err != nil { + return nil, fmt.Errorf("error downloading file: %w", err) + } + + err = extractTarball(tgzFile, tmpDir, b.logger) + if err != nil { + return nil, fmt.Errorf("error extracting tgz file: %w", err) + } + + binDir := filepath.Join(tmpDir, "kubernetes/server/bin") + contents, err := os.ReadFile(filepath.Join(binDir, "kube-apiserver.docker_tag")) + if err != nil { + return nil, err + } + sourceVersionRaw := strings.TrimSpace(string(contents)) + return &bits{ + binaryPaths: []string{ + filepath.Join(binDir, "kubeadm"), + filepath.Join(binDir, "kubelet"), + filepath.Join(binDir, "kubectl"), + }, + imagePaths: []string{ + filepath.Join(binDir, "kube-apiserver.tar"), + filepath.Join(binDir, "kube-controller-manager.tar"), + filepath.Join(binDir, "kube-scheduler.tar"), + filepath.Join(binDir, "kube-proxy.tar"), + }, + version: sourceVersionRaw, + }, nil +} + +func (b *remoteBuilder) downloadURL(url string, destPath string) error { + output, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("error creating file for download %q: %v", destPath, err) + } + defer output.Close() + + b.logger.V(0).Infof("Downloading %q", url) + + // Create a client with custom timeouts + // to avoid idle downloads to hang the program + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + IdleConnTimeout: 30 * time.Second, + }, + } + + // this will stop slow downloads after 3 minutes + // and interrupt reading of the Response.Body + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("cannot create request: %v", err) + } + + response, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("error doing HTTP fetch of %q: %v", url, err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("error response from %q: HTTP %v", url, response.StatusCode) + } + + start := time.Now() + defer func() { + b.logger.V(2).Infof("Copying %q to %q took %q", url, destPath, time.Since(start)) + }() + + _, err = io.Copy(output, response.Body) + if err != nil { + return fmt.Errorf("error downloading HTTP content from %q: %v", url, err) + } + return nil +} + +func extractTarball(tarPath, destDirectory string, logger log.Logger) (err error) { + // Open the tar file + f, err := os.Open(tarPath) + if err != nil { + return fmt.Errorf("opening tarball: %w", err) + } + defer f.Close() + + gzipReader, err := gzip.NewReader(f) + if err != nil { + return err + } + tr := tar.NewReader(gzipReader) + + numFiles := 0 + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("reading tarfile %s: %w", tarPath, err) + } + + if hdr.FileInfo().IsDir() { + continue + } + + if err := os.MkdirAll( + filepath.Join(destDirectory, filepath.Dir(hdr.Name)), os.FileMode(0o755), + ); err != nil { + return fmt.Errorf("creating image directory structure: %w", err) + } + + f, err := os.Create(filepath.Join(destDirectory, hdr.Name)) + if err != nil { + return fmt.Errorf("creating image layer file: %w", err) + } + + if _, err := io.CopyN(f, tr, hdr.Size); err != nil { + f.Close() + if err == io.EOF { + break + } + + return fmt.Errorf("extracting image data: %w", err) + } + f.Close() + + numFiles++ + } + + logger.V(2).Infof("Successfully extracted %d files from image tarball %s", numFiles, tarPath) + return err +} diff --git a/pkg/build/nodeimage/internal/kube/builder_tarball.go b/pkg/build/nodeimage/internal/kube/builder_tarball.go new file mode 100644 index 0000000000..47417f59f1 --- /dev/null +++ b/pkg/build/nodeimage/internal/kube/builder_tarball.go @@ -0,0 +1,78 @@ +/* +Copyright 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 kube + +import ( + "fmt" + "os" + "path/filepath" + "sigs.k8s.io/kind/pkg/log" + "strings" +) + +// TODO(bentheelder): plumb through arch + +// directoryBuilder implements Bits for a local docker-ized make / bash build +type directoryBuilder struct { + tarballPath string + logger log.Logger +} + +var _ Builder = &directoryBuilder{} + +// NewTarballBuilder returns a new Bits backed by the docker-ized build, +// given kubeRoot, the path to the kubernetes source directory +func NewTarballBuilder(logger log.Logger, tarballPath string) (Builder, error) { + return &directoryBuilder{ + tarballPath: tarballPath, + logger: logger, + }, nil +} + +// Build implements Bits.Build +func (b *directoryBuilder) Build() (Bits, error) { + tmpDir, err := os.MkdirTemp(os.TempDir(), "k8s-tar-extract-") + if err != nil { + return nil, fmt.Errorf("error creating temporary directory for tar extraction: %w", err) + } + + err = extractTarball(b.tarballPath, tmpDir, b.logger) + if err != nil { + return nil, fmt.Errorf("error extracting tar file: %w", err) + } + + binDir := filepath.Join(tmpDir, "kubernetes/server/bin") + contents, err := os.ReadFile(filepath.Join(binDir, "kube-apiserver.docker_tag")) + if err != nil { + return nil, err + } + sourceVersionRaw := strings.TrimSpace(string(contents)) + return &bits{ + binaryPaths: []string{ + filepath.Join(binDir, "kubeadm"), + filepath.Join(binDir, "kubelet"), + filepath.Join(binDir, "kubectl"), + }, + imagePaths: []string{ + filepath.Join(binDir, "kube-apiserver.tar"), + filepath.Join(binDir, "kube-controller-manager.tar"), + filepath.Join(binDir, "kube-scheduler.tar"), + filepath.Join(binDir, "kube-proxy.tar"), + }, + version: sourceVersionRaw, + }, nil +} diff --git a/pkg/build/nodeimage/options.go b/pkg/build/nodeimage/options.go index 88906e25a5..6d95fc4060 100644 --- a/pkg/build/nodeimage/options.go +++ b/pkg/build/nodeimage/options.go @@ -47,10 +47,10 @@ func WithBaseImage(image string) Option { }) } -// WithKuberoot sets the path to the Kubernetes source directory (if empty, the path will be autodetected) -func WithKuberoot(root string) Option { +// WithKubeParam sets the path to the Kubernetes source directory (if empty, the path will be autodetected) +func WithKubeParam(root string) Option { return optionAdapter(func(b *buildContext) error { - b.kubeRoot = root + b.kubeParam = root return nil }) } diff --git a/pkg/cmd/kind/build/nodeimage/nodeimage.go b/pkg/cmd/kind/build/nodeimage/nodeimage.go index ebe89c0b5b..d2a5289c7f 100644 --- a/pkg/cmd/kind/build/nodeimage/nodeimage.go +++ b/pkg/cmd/kind/build/nodeimage/nodeimage.go @@ -50,9 +50,6 @@ func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command { } logger.Warn("--kube-root is deprecated, please switch to passing this as an argument") } - if cmd.Flags().Lookup("type").Changed { - return errors.New("--type is no longer supported, please remove this flag") - } return runE(logger, flags, args) }, } @@ -97,7 +94,7 @@ func runE(logger log.Logger, flags *flagpole, args []string) error { if err := nodeimage.Build( nodeimage.WithImage(flags.Image), nodeimage.WithBaseImage(flags.BaseImage), - nodeimage.WithKuberoot(kubeRoot), + nodeimage.WithKubeParam(kubeRoot), nodeimage.WithLogger(logger), nodeimage.WithArch(flags.Arch), ); err != nil {