From a7864e5e618ca9f5691d35d64e677f7ee2b06a47 Mon Sep 17 00:00:00 2001 From: Mallikarjuna Date: Fri, 27 Mar 2026 23:24:24 -0400 Subject: [PATCH] fix(helm): allow helm v4 binary in airgapped environments --- fetch_issues.py | 12 ++ .../build/builder/buildkit/buildkit.go | 92 +++++++------- pkg/devspace/build/builder/helper/helper.go | 100 +++++++-------- .../build/builder/helper/helper_test.go | 18 +-- pkg/devspace/build/builder/helper/util.go | 80 ++++++------ .../build/builder/kaniko/build_pod.go | 94 +++++++------- pkg/devspace/build/builder/kaniko/kaniko.go | 118 +++++++++--------- .../build/builder/localregistry/build.go | 46 +++---- pkg/devspace/configure/image.go | 88 ++++++------- pkg/devspace/docker/auth_test.go | 62 ++++----- pkg/devspace/docker/cli.go | 16 +-- pkg/devspace/docker/client.go | 52 ++++---- pkg/devspace/docker/images_test.go | 14 +-- pkg/devspace/docker/testing/fake.go | 4 +- pkg/devspace/helm/downloader/downloader.go | 31 +++++ .../helm/downloader/downloader_test.go | 34 +++++ pkg/devspace/helm/v3/client.go | 4 +- .../pipeline/engine/basichandler/handler.go | 3 +- pkg/devspace/pullsecrets/util.go | 8 +- pkg/util/dockerfile/get.go | 26 ++-- 20 files changed, 490 insertions(+), 412 deletions(-) create mode 100644 fetch_issues.py create mode 100644 pkg/devspace/helm/downloader/downloader.go create mode 100644 pkg/devspace/helm/downloader/downloader_test.go diff --git a/fetch_issues.py b/fetch_issues.py new file mode 100644 index 0000000000..c86c443811 --- /dev/null +++ b/fetch_issues.py @@ -0,0 +1,12 @@ +import urllib.request, json +req = urllib.request.Request("https://api.github.com/repos/devspace-sh/devspace/issues?state=open&sort=created&direction=desc&per_page=40") +req.add_header('User-Agent', 'python-urllib') +try: + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + for issue in data: + if issue['number'] in [3179, 3174, 3106]: + print(f"--- ISSUE #{issue['number']} ---") + print(issue['body'][:1000]) +except Exception as e: + print(e) diff --git a/pkg/devspace/build/builder/buildkit/buildkit.go b/pkg/devspace/build/builder/buildkit/buildkit.go index 2434d97f79..25155a1e24 100644 --- a/pkg/devspace/build/builder/buildkit/buildkit.go +++ b/pkg/devspace/build/builder/buildkit/buildkit.go @@ -12,14 +12,15 @@ import ( "strconv" "strings" "time" - + "github.com/loft-sh/devspace/pkg/devspace/pipeline/env" "mvdan.cc/sh/v3/expand" - + devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" command2 "github.com/loft-sh/utils/pkg/command" - + cliconfig "github.com/docker/cli/cli/config" + "github.com/docker/docker/api/types/build" "github.com/loft-sh/devspace/pkg/devspace/build/builder/helper" "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" dockerpkg "github.com/loft-sh/devspace/pkg/devspace/docker" @@ -27,7 +28,6 @@ import ( logpkg "github.com/loft-sh/devspace/pkg/util/log" "github.com/pkg/errors" "k8s.io/client-go/tools/clientcmd" - "github.com/docker/docker/api/types/build" ) // EngineName is the name of the building engine @@ -49,7 +49,7 @@ func NewBuilder(ctx devspacecontext.Context, imageConf *latest.Image, imageTags return nil, err } } - + return &Builder{ helper: helper.NewBuildHelper(ctx, EngineName, imageConf, imageTags), skipPush: skipPush, @@ -68,7 +68,7 @@ func (b *Builder) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) imageCache, _ := ctx.Config().LocalCache().GetImageCache(b.helper.ImageConf.Name) imageName := imageCache.ResolveImage() + ":" + imageCache.Tag rebuild, err := b.helper.ShouldRebuild(ctx, forceRebuild) - + // Check if image is present in local docker daemon if !rebuild && err == nil && b.helper.ImageConf.BuildKit.InCluster == nil { if b.skipPushOnLocalKubernetes && ctx.KubeClient() != nil && kubectl.IsLocalKubernetes(ctx.KubeClient()) { @@ -76,7 +76,7 @@ func (b *Builder) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) if err != nil { return false, err } - + found, err := b.helper.IsImageAvailableLocally(ctx, dockerClient) if !found && err == nil { ctx.Log().Infof("Rebuild image %s because it was not found in local docker daemon", imageName) @@ -84,7 +84,7 @@ func (b *Builder) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) } } } - + return rebuild, err } @@ -93,32 +93,32 @@ func (b *Builder) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) // dockerfilePath is the absolute path to the dockerfile WITHIN the contextPath func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfilePath string, entrypoint []string, cmd []string) error { buildKitConfig := b.helper.ImageConf.BuildKit - + // create the builder builder, err := ensureBuilder(ctx.Context(), ctx.WorkingDir(), ctx.Environ(), ctx.KubeClient(), buildKitConfig, ctx.Log()) if err != nil { return err } - + // create the context stream body, writer, _, buildOptions, err := b.helper.CreateContextStream(contextPath, dockerfilePath, entrypoint, cmd, ctx.Log()) defer writer.Close() if err != nil { return err } - + // We skip pushing when it is the minikube client usingLocalKubernetes := ctx.KubeClient() != nil && kubectl.IsLocalKubernetes(ctx.KubeClient()) if b.skipPushOnLocalKubernetes && usingLocalKubernetes { b.skipPush = true } - + // Should we use the minikube docker daemon? useMinikubeDocker := false if ctx.KubeClient() != nil && kubectl.IsMinikubeKubernetes(ctx.KubeClient()) && (buildKitConfig.PreferMinikube == nil || *buildKitConfig.PreferMinikube) { useMinikubeDocker = true } - + // Should we build with cli? skipPush := b.skipPush || b.helper.ImageConf.SkipPush return buildWithCLI(ctx.Context(), ctx.WorkingDir(), ctx.Environ(), body, writer, ctx.KubeClient(), builder, buildKitConfig, *buildOptions, useMinikubeDocker, skipPush, ctx.Log()) @@ -129,14 +129,14 @@ func buildWithCLI(ctx context.Context, dir string, environ expand.Environ, conte if len(imageConf.Command) > 0 { command = imageConf.Command } - + args := []string{"build"} if options.BuildArgs != nil { for k, v := range options.BuildArgs { if v == nil { continue } - + args = append(args, "--build-arg", k+"="+*v) } } @@ -167,9 +167,9 @@ func buildWithCLI(ctx context.Context, dir string, environ expand.Environ, conte return err } defer os.Remove(tempFile) - + args = append(args, "--builder", builder) - + // TODO: find a better solution than this // we wait here a little bit, otherwise it might be possible that we get issues during // parallel image building, as it seems that docker buildx has problems if the @@ -178,14 +178,14 @@ func buildWithCLI(ctx context.Context, dir string, environ expand.Environ, conte time.Sleep(time.Millisecond * time.Duration(rand.Intn(3000)+500)) } args = append(args, imageConf.Args...) - + args = append(args, "-") - + log.Infof("Execute BuildKit command with: %s %s", strings.Join(command, " "), strings.Join(args, " ")) completeArgs := []string{} completeArgs = append(completeArgs, command[1:]...) completeArgs = append(completeArgs, args...) - + var ( minikubeEnv map[string]string err error @@ -200,7 +200,7 @@ func buildWithCLI(ctx context.Context, dir string, environ expand.Environ, conte if err != nil { return err } - + if skipPush && kubeClient != nil && kubectl.GetKindContext(kubeClient.CurrentContext()) != "" { // Load image if it is a kind-context for _, tag := range options.Tags { @@ -214,7 +214,7 @@ func buildWithCLI(ctx context.Context, dir string, environ expand.Environ, conte log.Info("Image loaded to kind cluster") } } - + return nil } @@ -240,27 +240,27 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro } else if kubeClient == nil { return "", fmt.Errorf("cannot build in cluster wth build kit without a correct kubernetes context") } - + namespace := kubeClient.Namespace() if imageConf.InCluster.Namespace != "" { namespace = imageConf.InCluster.Namespace } - + name := "devspace-" + namespace if imageConf.InCluster.Name != "" { name = imageConf.InCluster.Name } - + // check if we should skip if imageConf.InCluster.NoCreate { return name, nil } - + command := []string{"docker", "buildx"} if len(imageConf.Command) > 0 { command = imageConf.Command } - + args := []string{"create", "--driver", "kubernetes", "--driver-opt", "namespace=" + namespace, "--name", name} if imageConf.InCluster.Rootless { args = append(args, "--driver-opt", "rootless=true") @@ -274,11 +274,11 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro if len(imageConf.InCluster.CreateArgs) > 0 { args = append(args, imageConf.InCluster.CreateArgs...) } - + completeArgs := []string{} completeArgs = append(completeArgs, command[1:]...) completeArgs = append(completeArgs, args...) - + // check if builder already exists builderPath := filepath.Join(getConfigStorePath(), "instances", name) _, err := os.Stat(builderPath) @@ -286,14 +286,14 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro if imageConf.InCluster.NoRecreate { return name, nil } - + // update the builder if necessary b, err := os.ReadFile(builderPath) if err != nil { log.Warnf("Error reading builder %s: %v", builderPath, err) return name, nil } - + // parse builder config ng := &NodeGroup{} err = json.Unmarshal(b, ng) @@ -301,11 +301,11 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro log.Warnf("Error decoding builder %s: %v", builderPath, err) return name, nil } - + // check for: correct driver name, driver opts if strings.ToLower(ng.Driver) == "kubernetes" && len(ng.Nodes) == 1 { node := ng.Nodes[0] - + // check driver options namespaceCorrect := node.DriverOpts["namespace"] == namespace if node.DriverOpts["rootless"] == "" { @@ -314,28 +314,28 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro rootlessCorrect := strconv.FormatBool(imageConf.InCluster.Rootless) == node.DriverOpts["rootless"] imageCorrect := imageConf.InCluster.Image == node.DriverOpts["image"] nodeSelectorCorrect := imageConf.InCluster.NodeSelector == node.DriverOpts["nodeselector"] - + // if builder up to date, exit here if namespaceCorrect && rootlessCorrect && imageCorrect && nodeSelectorCorrect { return name, nil } } - + // recreate the builder log.Infof("Recreate BuildKit builder because builder options differ") - + // create a temporary kube context tempFile, err := tempKubeContextFromClient(kubeClient) if err != nil { return "", err } defer os.Remove(tempFile) - + // prepare the command rmArgs := []string{} rmArgs = append(rmArgs, command[1:]...) rmArgs = append(rmArgs, "rm", name) - + // execute the command out, err := command2.CombinedOutput(ctx, workingDir, env.NewVariableEnvProvider(environ, map[string]string{ "KUBECONFIG": tempFile, @@ -344,10 +344,10 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro log.Warnf("error deleting BuildKit builder: %s => %v", string(out), err) } } - + // create the builder log.Infof("Create BuildKit builder with: %s %s", strings.Join(command, " "), strings.Join(args, " ")) - + // This is necessary because docker would otherwise save the used kube config // which we don't want because we will override it with our own temp kube config // during building. @@ -359,7 +359,7 @@ func ensureBuilder(ctx context.Context, workingDir string, environ expand.Enviro return "", fmt.Errorf("error creating BuildKit builder: %s => %v", string(out), err) } } - + return name, nil } @@ -370,7 +370,7 @@ func getConfigStorePath() string { if buildxConfig := os.Getenv("BUILDX_CONFIG"); buildxConfig != "" { return buildxConfig } - + stderr := &bytes.Buffer{} configFile := cliconfig.LoadDefaultConfigFile(stderr) buildxConfig := filepath.Join(filepath.Dir(configFile.Filename), "buildx") @@ -385,21 +385,21 @@ func tempKubeContextFromClient(kubeClient kubectl.Client) (string, error) { if !kubeClient.IsInCluster() { rawConfig.CurrentContext = kubeClient.CurrentContext() } - + bytes, err := clientcmd.Write(rawConfig) if err != nil { return "", err } - + tempFile, err := os.CreateTemp("", "") if err != nil { return "", err } - + _, err = tempFile.Write(bytes) if err != nil { return "", errors.Wrap(err, "error writing to file") } - + return tempFile.Name(), nil } diff --git a/pkg/devspace/build/builder/helper/helper.go b/pkg/devspace/build/builder/helper/helper.go index e83cb48f51..667ee4a825 100644 --- a/pkg/devspace/build/builder/helper/helper.go +++ b/pkg/devspace/build/builder/helper/helper.go @@ -5,10 +5,10 @@ import ( "os" "path/filepath" "strings" - + + "github.com/containers/storage/pkg/idtools" "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/image" - "github.com/containers/storage/pkg/idtools" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/loft-sh/devspace/pkg/devspace/build/builder/restart" @@ -16,9 +16,10 @@ import ( logpkg "github.com/loft-sh/devspace/pkg/util/log" dockerterm "github.com/moby/term" "github.com/sirupsen/logrus" - - "github.com/docker/cli/cli/command/image/build" + "github.com/containers/storage/pkg/archive" + "github.com/docker/cli/cli/command/image/build" + buildtypes "github.com/docker/docker/api/types/build" "github.com/loft-sh/devspace/pkg/devspace/config/localcache" "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" @@ -28,7 +29,6 @@ import ( "github.com/loft-sh/utils/pkg/command" "github.com/pkg/errors" "gopkg.in/yaml.v3" - buildtypes "github.com/docker/docker/api/types/build" ) var ( @@ -38,10 +38,10 @@ var ( // BuildHelper is the helper class to store common functionality used by both the docker and kaniko builder type BuildHelper struct { ImageConf *latest.Image - + DockerfilePath string ContextPath string - + EngineName string ImageName string ImageTags []string @@ -60,30 +60,30 @@ func NewBuildHelper(ctx devspacecontext.Context, engineName string, imageConf *l dockerfilePath, contextPath = GetDockerfileAndContext(ctx, imageConf) imageName = imageConf.Image ) - + // Check if we should overwrite entrypoint var ( entrypoint []string cmd []string ) - + if imageConf.Entrypoint != nil { entrypoint = imageConf.Entrypoint } if imageConf.Cmd != nil { cmd = imageConf.Cmd } - + return &BuildHelper{ ImageConf: imageConf, - + DockerfilePath: dockerfilePath, ContextPath: contextPath, - + ImageName: imageName, ImageTags: imageTags, EngineName: engineName, - + Entrypoint: entrypoint, Cmd: cmd, } @@ -92,13 +92,13 @@ func NewBuildHelper(ctx devspacecontext.Context, engineName string, imageConf *l // Build builds a new image func (b *BuildHelper) Build(ctx devspacecontext.Context, imageBuilder BuildHelperInterface) error { ctx.Log().Infof("Building image '%s:%s' with engine '%s'", b.ImageName, b.ImageTags[0], b.EngineName) - + // Build Image err := imageBuilder.BuildImage(ctx, b.ContextPath, b.DockerfilePath, b.Entrypoint, b.Cmd) if err != nil { return err } - + ctx.Log().Done("Done processing image '" + b.ImageName + "'") return nil } @@ -106,13 +106,13 @@ func (b *BuildHelper) Build(ctx devspacecontext.Context, imageBuilder BuildHelpe // ShouldRebuild determines if the image should be rebuilt func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) (bool, error) { imageCache, _ := ctx.Config().LocalCache().GetImageCache(b.ImageConf.Name) - + // if rebuild strategy is always, we return here if b.ImageConf.RebuildStrategy == latest.RebuildStrategyAlways { ctx.Log().Infof("Rebuild image %s because strategy is always rebuild", imageCache.ImageName) return true, nil } - + // Hash dockerfile _, err := os.Stat(b.DockerfilePath) if err != nil { @@ -122,15 +122,15 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo if err != nil { return false, errors.Wrap(err, "hash dockerfile") } - + // Hash image config configStr, err := yaml.Marshal(*b.ImageConf) if err != nil { return false, errors.Wrap(err, "marshal image config") } - + imageConfigHash := hash.String(string(configStr)) - + // Hash entrypoint entrypointHash := "" if len(b.Entrypoint) > 0 { @@ -146,7 +146,7 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo if entrypointHash != "" { entrypointHash = hash.String(entrypointHash) } - + // only rebuild Docker image when Dockerfile or context has changed since latest build mustRebuild := imageCache.Tag == "" || imageCache.DockerfileHash != dockerfileHash || imageCache.ImageConfigHash != imageConfigHash || imageCache.EntrypointHash != entrypointHash if imageCache.Tag == "" { @@ -158,7 +158,7 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo } else if imageCache.EntrypointHash != entrypointHash { ctx.Log().Infof("Rebuild image %s because entrypoint has changed", imageCache.ImageName) } - + var lastContextClient kubectl.Client if ctx.Config().LocalCache().GetLastContext() != nil { lastContextClient, err = kubectl.NewClientFromContext( @@ -171,7 +171,7 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo return false, err } } - + // Okay this check verifies if the previous deploy context was local kubernetes context where we didn't push the image and now have a kubernetes context where we probably push // or use another docker client (e.g. minikube <-> docker-desktop) if !mustRebuild && @@ -186,7 +186,7 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo Context: ctx.KubeClient().CurrentContext(), }) } - + // Check if should consider context path changes for rebuilding if b.ImageConf.RebuildStrategy != latest.RebuildStrategyIgnoreContextChanges { // Hash context path @@ -194,7 +194,7 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo if err != nil { return false, errors.Wrap(err, "get context from local dir") } - + relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { return false, errors.Errorf("Error getting tar name: %v", err) @@ -203,17 +203,17 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo if err != nil { return false, errors.Errorf("Error reading .dockerignore: %v", err) } - + contextHash, err := hash.DirectoryExcludes(contextDir, excludes, false) if err != nil { return false, errors.Errorf("Error hashing %s: %v", contextDir, err) } - + if !mustRebuild && imageCache.ContextHash != contextHash { ctx.Log().Infof("Rebuild image %s because build context has changed", imageCache.ImageName) } mustRebuild = mustRebuild || imageCache.ContextHash != contextHash - + // TODO: This is not an ideal solution since there can be the issue that the user runs // devspace dev & the generated.yaml is written without ContextHash and on a subsequent // devspace deploy the image would be rebuild, because the ContextHash was empty and is @@ -223,13 +223,13 @@ func (b *BuildHelper) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bo imageCache.ContextHash = contextHash } } - + if forceRebuild || mustRebuild { imageCache.DockerfileHash = dockerfileHash imageCache.ImageConfigHash = imageConfigHash imageCache.EntrypointHash = entrypointHash } - + ctx.Config().LocalCache().SetImageCache(b.ImageConf.Name, imageCache) return mustRebuild, nil } @@ -243,10 +243,10 @@ func (b *BuildHelper) IsImageAvailableLocally(ctx devspacecontext.Context, docke if err != nil { return true, nil } - + imageCache, _ := ctx.Config().LocalCache().GetImageCache(b.ImageConf.Name) imageName := imageCache.ResolveImage() + ":" + imageCache.Tag - + dockerAPIClient := dockerClient.DockerAPIClient() imageList, err := dockerAPIClient.ImageList(ctx.Context(), image.ListOptions{}) if err != nil { @@ -276,7 +276,7 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if b.ImageConf.Network != "" { options.NetworkMode = b.ImageConf.Network } - + // Determine output writer var writer io.WriteCloser if log == logpkg.GetInstance() { @@ -284,12 +284,12 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en } else { writer = log.Writer(logrus.InfoLevel, false) } - + contextDir, relDockerfile, err := build.GetContextFromLocalDir(contextPath, dockerfilePath) if err != nil { return nil, writer, nil, nil, err } - + // Dockerfile is out of context var dockerfileCtx *os.File if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { @@ -300,7 +300,7 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en } defer dockerfileCtx.Close() } - + // And canonicalize dockerfile name to a platform-independent one authConfigs, _ := dockerclient.GetAllAuthConfigs() relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) @@ -311,11 +311,11 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if err != nil { return nil, writer, nil, nil, err } - + if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { return nil, writer, nil, nil, errors.Errorf("Error checking context: '%s'", err) } - + buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{ ExcludePatterns: excludes, ChownOpts: &idtools.IDPair{UID: 0, GID: 0}, @@ -323,7 +323,7 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if err != nil { return nil, writer, nil, nil, err } - + // Check if we should overwrite entrypoint injectRestartHelper := b.ImageConf.InjectRestartHelper || b.ImageConf.InjectLegacyRestartHelper if len(entrypoint) > 0 || len(cmd) > 0 || injectRestartHelper || len(b.ImageConf.AppendDockerfileInstructions) > 0 { @@ -331,7 +331,7 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if err != nil { return nil, writer, nil, nil, err } - + // Check if dockerfile is out of context, then we use the docker way to replace the dockerfile if dockerfileCtx != nil { // We will add it to the build context @@ -339,7 +339,7 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if err != nil { return nil, writer, nil, nil, errors.Errorf("unable to open Dockerfile: %v", err) } - + defer dockerfileCtx.Close() } else { // We will add it to the build context @@ -347,20 +347,20 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en if err != nil { return nil, writer, nil, nil, errors.Errorf("unable to open Dockerfile: %v", err) } - + buildCtx, err = OverwriteDockerfileInBuildContext(overwriteDockerfileCtx, buildCtx, relDockerfile) if err != nil { return nil, writer, nil, nil, errors.Errorf("Error overwriting %s: %v", relDockerfile, err) } } - + defer os.RemoveAll(filepath.Dir(dockerfilePath)) - + // inject the build script if injectRestartHelper { var helperScript string var err error - + if b.ImageConf.InjectRestartHelper { helperScript, err = restart.LoadRestartHelper(b.ImageConf.RestartHelperPath) if err != nil { @@ -372,14 +372,14 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en return nil, writer, nil, nil, errors.Wrap(err, "load legacy restart helper") } } - + buildCtx, err = InjectBuildScriptInContext(helperScript, buildCtx) if err != nil { return nil, writer, nil, nil, errors.Wrap(err, "inject build script into context") } } } - + // replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context if dockerfileCtx != nil && buildCtx != nil { buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx) @@ -387,13 +387,13 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en return nil, writer, nil, nil, err } } - + // Which tags to build tags := []string{} for _, tag := range b.ImageTags { tags = append(tags, b.ImageName+":"+tag) } - + // Setup an upload progress bar outStream := streams.NewOut(writer) progressOutput := streamformatter.NewProgressOutput(outStream) @@ -406,6 +406,6 @@ func (b *BuildHelper) CreateContextStream(contextPath, dockerfilePath string, en NetworkMode: options.NetworkMode, AuthConfigs: authConfigs, } - + return body, writer, outStream, buildOptions, nil } diff --git a/pkg/devspace/build/builder/helper/helper_test.go b/pkg/devspace/build/builder/helper/helper_test.go index 7de59f95fa..bbeaae408b 100644 --- a/pkg/devspace/build/builder/helper/helper_test.go +++ b/pkg/devspace/build/builder/helper/helper_test.go @@ -4,20 +4,20 @@ import ( "context" "os/exec" "testing" - + "github.com/docker/docker/api/types/image" "github.com/loft-sh/devspace/pkg/devspace/config" "github.com/loft-sh/devspace/pkg/devspace/config/localcache" "github.com/loft-sh/devspace/pkg/devspace/config/remotecache" devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" "github.com/loft-sh/devspace/pkg/util/log" - + "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" "github.com/loft-sh/devspace/pkg/devspace/docker" "gotest.tools/assert" - - dockerclient "github.com/docker/docker/client" + "github.com/docker/docker/api/types/checkpoint" + dockerclient "github.com/docker/docker/client" ) type fakeDockerClient struct { @@ -64,9 +64,9 @@ func TestIsImageAvailableLocally(t *testing.T) { }, Entrypoint: []string{"echo"}, } - + client := &fakeDockerClient{} - + cache1 := &localcache.LocalCache{ Images: map[string]localcache.ImageCache{ "ImageConf": { @@ -80,7 +80,7 @@ func TestIsImageAvailableLocally(t *testing.T) { t.Error(err) } assert.Assert(t, exists1, "Expected image1:dbysxsH to be available locally") - + cache2 := &localcache.LocalCache{ Images: map[string]localcache.ImageCache{ "ImageConf": { @@ -94,7 +94,7 @@ func TestIsImageAvailableLocally(t *testing.T) { t.Error(err) } assert.Assert(t, exists2, "Expected image1:xEmrClh to be available locally") - + cache3 := &localcache.LocalCache{ Images: map[string]localcache.ImageCache{ "ImageConf": { @@ -108,7 +108,7 @@ func TestIsImageAvailableLocally(t *testing.T) { t.Error(err) } assert.Assert(t, exists3, "Expected image1:UgjIYde to be available locally") - + cache4 := &localcache.LocalCache{ Images: map[string]localcache.ImageCache{ "ImageConf": { diff --git a/pkg/devspace/build/builder/helper/util.go b/pkg/devspace/build/builder/helper/util.go index 09953a7fc6..edb5353c29 100644 --- a/pkg/devspace/build/builder/helper/util.go +++ b/pkg/devspace/build/builder/helper/util.go @@ -12,17 +12,17 @@ import ( "regexp" "strings" "time" - + scanner2 "github.com/loft-sh/devspace/pkg/util/scanner" "github.com/moby/buildkit/frontend/dockerfile/dockerignore" - + "github.com/loft-sh/devspace/pkg/devspace/build/builder/restart" - + logpkg "github.com/loft-sh/devspace/pkg/util/log" - + + "github.com/containers/storage/pkg/archive" "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" "github.com/pkg/errors" - "github.com/containers/storage/pkg/archive" ) // DefaultDockerfilePath is the default dockerfile path to use @@ -65,7 +65,7 @@ func ReadDockerignore(contextDir string, dockerfile string) ([]string, error) { return nil, err } defer f.Close() - + excludes, err = dockerignore.ReadAll(f) if err != nil { return nil, err @@ -95,15 +95,15 @@ func GetDockerfileAndContext(ctx devspacecontext.Context, imageConf *latest.Imag dockerfilePath = DefaultDockerfilePath contextPath = DefaultContextPath ) - + if imageConf.Dockerfile != "" { dockerfilePath = imageConf.Dockerfile } - + if imageConf.Context != "" { contextPath = imageConf.Context } - + return ctx.ResolvePath(dockerfilePath), ctx.ResolvePath(contextPath) } @@ -127,7 +127,7 @@ func InjectBuildScriptInContext(helperScript string, buildCtx io.ReadCloser) (io ChangeTime: now, Typeflag: tar.TypeDir, } - + buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ "/.devspace/.devspace": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { return fldTmpl, nil, nil @@ -156,7 +156,7 @@ func OverwriteDockerfileInBuildContext(dockerfileCtx io.ReadCloser, buildCtx io. AccessTime: now, ChangeTime: now, } - + buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ // Overwrite docker file relDockerfile: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { @@ -175,18 +175,18 @@ func RewriteDockerfile(dockerfile string, entrypoint []string, cmd []string, add if additionalInstructions == nil { additionalInstructions = []string{} } - + if injectHelper { data, err := os.ReadFile(dockerfile) if err != nil { return "", err } - + oldEntrypoint, oldCmd, err := GetEntrypointAndCmd(string(data), target) if err != nil { return "", err } - + if len(entrypoint) == 0 { if len(oldEntrypoint) == 0 { if len(cmd) == 0 && len(oldCmd) == 0 { @@ -194,7 +194,7 @@ func RewriteDockerfile(dockerfile string, entrypoint []string, cmd []string, add } log.Warn("Using CMD statement for injecting restart helper because ENTRYPOINT is missing in Dockerfile and `images.*.entrypoint` is also not configured") } - + entrypoint = oldEntrypoint if len(cmd) == 0 && len(oldCmd) > 0 { cmd = oldCmd @@ -202,11 +202,11 @@ func RewriteDockerfile(dockerfile string, entrypoint []string, cmd []string, add } else if len(cmd) == 0 && len(oldCmd) > 0 { cmd = oldCmd } - + entrypoint = append([]string{restart.ScriptPath}, entrypoint...) additionalInstructions = append(additionalInstructions, "COPY /.devspace /") } - + return CreateTempDockerfile(dockerfile, entrypoint, cmd, additionalInstructions, target) } @@ -215,62 +215,62 @@ func CreateTempDockerfile(dockerfile string, entrypoint []string, cmd []string, if entrypoint == nil && cmd == nil && len(additionalLines) == 0 { return "", errors.New("entrypoint, cmd & additional lines are empty") } - + data, err := os.ReadFile(dockerfile) if err != nil { return "", err } - + // Overwrite entrypoint and cmd tmpDir, err := os.MkdirTemp("", "example") if err != nil { return "", err } - + // add the new entrypoint newData, err := addNewEntrypoint(string(data), entrypoint, cmd, additionalLines, target) if err != nil { return "", errors.Wrap(err, "add entrypoint") } - + tmpfn := filepath.Join(tmpDir, "Dockerfile") if err := os.WriteFile(tmpfn, []byte(newData), 0666); err != nil { return "", err } - + return tmpfn, nil } // GetDockerfileTargets returns an array of names of all targets defined in a given Dockerfile func GetDockerfileTargets(dockerfile string) ([]string, error) { targets := []string{} - + if dockerfile == "" { dockerfile = DefaultDockerfilePath } - + data, err := os.ReadFile(dockerfile) if err != nil { return targets, err } content := string(data) - + // Find all targets targetFinder, err := regexp.Compile(fmt.Sprintf(DockerfileTargetRegexTemplate, "\\S+")) if err != nil { return targets, err } - + rawTargets := targetFinder.FindAllStringSubmatch(content, -1) for _, target := range rawTargets { entrypoint, cmd, err := GetEntrypointAndCmd(content, target[3]) if err != nil || (len(entrypoint) == 0 && len(cmd) == 0) { continue } - + targets = append(targets, target[3]) } - + return targets, nil } @@ -291,16 +291,16 @@ func addNewEntrypoint(content string, entrypoint []string, cmd []string, additio } else if cmd != nil { entrypointStr += "\n\nCMD []\n" } - + if target == "" { return content + entrypointStr, nil } - + before, after, err := splitDockerfileAtTarget(content, target) if err != nil { return "", err } - + return before + entrypointStr + after, nil } @@ -310,20 +310,20 @@ func splitDockerfileAtTarget(content string, target string) (string, string, err if err != nil { return "", "", err } - + matches := targetFinder.FindAllStringIndex(content, -1) if len(matches) == 0 { return "", "", errors.Errorf("Coulnd't find target '%s' in dockerfile", target) } else if len(matches) > 1 { return "", "", errors.Errorf("Multiple matches for target '%s' in dockerfile", target) } - + // Find the next FROM statement nextFrom := nextFromFinder.FindStringIndex(content[matches[0][1]:]) if len(nextFrom) != 2 { return content, "", nil } - + return content[:matches[0][1]+nextFrom[0]], content[matches[0][1]+nextFrom[0]:], nil } @@ -334,23 +334,23 @@ func GetEntrypointAndCmd(content string, target string) ([]string, []string, err if target == "" { return parseLastOccurence(content) } - + before, _, err := splitDockerfileAtTarget(content, target) if err != nil { return nil, nil, err } - + return parseLastOccurence(before) } func parseLastOccurence(content string) ([]string, []string, error) { scanner := scanner2.NewScanner(strings.NewReader(content)) - + var lastOccurenceEntrypoint []string var lastOccurenceCmd []string for scanner.Scan() { line := scanner.Text() - + // is ENTRYPOINT? if matches := entrypointLinePattern.FindStringSubmatch(line); len(matches) == 2 { // exec or shell form? @@ -363,7 +363,7 @@ func parseLastOccurence(content string) ([]string, []string, error) { } else { lastOccurenceEntrypoint = []string{"/bin/sh", "-c", matches[1]} } - + // reset CMD lastOccurenceCmd = nil } else if matches := cmdLinePattern.FindStringSubmatch(line); len(matches) == 2 { @@ -379,6 +379,6 @@ func parseLastOccurence(content string) ([]string, []string, error) { } } } - + return lastOccurenceEntrypoint, lastOccurenceCmd, scanner.Err() } diff --git a/pkg/devspace/build/builder/kaniko/build_pod.go b/pkg/devspace/build/builder/kaniko/build_pod.go index 06ffc3e5ad..87abbfe432 100644 --- a/pkg/devspace/build/builder/kaniko/build_pod.go +++ b/pkg/devspace/build/builder/kaniko/build_pod.go @@ -3,21 +3,21 @@ package kaniko import ( "fmt" "path/filepath" - + "github.com/loft-sh/devspace/pkg/devspace/build/builder/kaniko/util" devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" - + "github.com/distribution/reference" "gopkg.in/yaml.v3" jsonyaml "sigs.k8s.io/yaml" - + "github.com/pkg/errors" k8sv1 "k8s.io/api/core/v1" - + + "github.com/docker/docker/api/types/build" "github.com/loft-sh/devspace/pkg/devspace/pullsecrets" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/docker/docker/api/types/build" ) // The kaniko init image that we use by default @@ -56,79 +56,79 @@ var defaultResources = &availableResources{ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, options *build.ImageBuildOptions, dockerfilePath string) (*k8sv1.Pod, error) { kanikoOptions := b.helper.ImageConf.Kaniko - + registryURL, err := pullsecrets.GetRegistryFromImageName(b.FullImageName) if err != nil { return nil, err } - + pullSecretName := pullsecrets.GetRegistryAuthSecretName(registryURL) if b.PullSecretName != "" { pullSecretName = b.PullSecretName } - + kanikoImage := kanikoBuildImage if kanikoOptions.Image != "" { kanikoImage = kanikoOptions.Image } - + kanikoInitImage := kanikoInitImage if kanikoOptions.InitImage != "" { kanikoInitImage = kanikoOptions.InitImage } - + kanikoPodGenerateName := podGenerateName if kanikoOptions.GenerateName != "" { kanikoPodGenerateName = kanikoOptions.GenerateName } - + // additional options to pass to kaniko kanikoArgs := []string{ "--dockerfile=" + kanikoContextPath + "/" + filepath.Base(dockerfilePath), "--context=dir://" + kanikoContextPath, } - + // specify destinations for _, tag := range b.helper.ImageTags { kanikoArgs = append(kanikoArgs, "--destination="+b.helper.ImageName+":"+tag) } - + // set target if options.Target != "" { kanikoArgs = append(kanikoArgs, "--target="+options.Target) } - + // set snapshot mode if kanikoOptions.SnapshotMode != "" { kanikoArgs = append(kanikoArgs, "--snapshotMode="+kanikoOptions.SnapshotMode) } else { kanikoArgs = append(kanikoArgs, "--snapshotMode=time") } - + // allow insecure registry if b.allowInsecureRegistry { kanikoArgs = append(kanikoArgs, "--insecure", "--skip-tls-verify") } - + // build args for key, value := range options.BuildArgs { newKanikoArg := fmt.Sprintf("%v=%v", key, *value) kanikoArgs = append(kanikoArgs, "--build-arg", newKanikoArg) } - + // cache flags if kanikoOptions.Cache { ref, err := reference.ParseNormalizedNamed(b.FullImageName) if err != nil { return nil, err } - + kanikoArgs = append(kanikoArgs, "--cache=true", "--cache-repo="+ref.Name()) } - + // extra flags kanikoArgs = append(kanikoArgs, kanikoOptions.Args...) - + // build the volumes volumes := []k8sv1.Volume{ { @@ -164,13 +164,13 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio MountPath: "/kaniko/.docker", }) } - + // add additional mounts for i, mount := range kanikoOptions.AdditionalMounts { volume := k8sv1.Volume{ Name: fmt.Sprintf("additional-volume-%d", i), } - + // check which volume type we got if mount.Secret != nil { volume.VolumeSource = k8sv1.VolumeSource{ @@ -180,7 +180,7 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio DefaultMode: mount.Secret.DefaultMode, }, } - + for _, item := range mount.Secret.Items { volume.VolumeSource.Secret.Items = append(volume.VolumeSource.Secret.Items, k8sv1.KeyToPath{ Key: item.Key, @@ -198,7 +198,7 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio DefaultMode: mount.ConfigMap.DefaultMode, }, } - + for _, item := range mount.ConfigMap.Items { volume.VolumeSource.ConfigMap.Items = append(volume.VolumeSource.ConfigMap.Items, k8sv1.KeyToPath{ Key: item.Key, @@ -209,7 +209,7 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio } else { continue } - + volumes = append(volumes, volume) volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ Name: volume.Name, @@ -262,35 +262,35 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio RestartPolicy: k8sv1.RestartPolicyNever, }, } - + // add extra annotations for k, v := range kanikoOptions.Annotations { pod.Annotations[k] = v } - + // add extra labels for k, v := range kanikoOptions.Labels { pod.Labels[k] = v } - + // add extra init env vars for k, v := range kanikoOptions.InitEnv { if len(pod.Spec.InitContainers[0].Env) == 0 { pod.Spec.InitContainers[0].Env = []k8sv1.EnvVar{} } - + pod.Spec.InitContainers[0].Env = append(pod.Spec.InitContainers[0].Env, k8sv1.EnvVar{ Name: k, Value: v, }) } - + // add extra env vars for k, v := range kanikoOptions.Env { if len(pod.Spec.Containers[0].Env) == 0 { pod.Spec.Containers[0].Env = []k8sv1.EnvVar{} } - + pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, k8sv1.EnvVar{ Name: k, Value: v, @@ -300,24 +300,24 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio if len(pod.Spec.Containers[0].Env) == 0 { pod.Spec.Containers[0].Env = []k8sv1.EnvVar{} } - + o, err := yaml.Marshal(v) if err != nil { return nil, errors.Errorf("error converting envFrom %s: %v", k, err) } - + source := &k8sv1.EnvVarSource{} err = jsonyaml.Unmarshal(o, source) if err != nil { return nil, errors.Errorf("error converting envFrom %s: %v", k, err) } - + pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, k8sv1.EnvVar{ Name: k, ValueFrom: source, }) } - + // check if we have specific options for the resources part if kanikoOptions.Resources == nil { // get available resources @@ -354,7 +354,7 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio if err != nil { return nil, errors.Wrap(err, "requests") } - + pod.Spec.InitContainers[0].Resources = k8sv1.ResourceRequirements{ Limits: limits, Requests: requests, @@ -364,7 +364,7 @@ func (b *Builder) getBuildPod(ctx devspacecontext.Context, buildID string, optio Requests: requests, } } - + // return the build pod return pod, nil } @@ -375,33 +375,33 @@ func (b *Builder) getAvailableResources(ctx devspacecontext.Context) (*available if err != nil { return nil, nil } - + availableResources := &availableResources{} - + // CPU availableResources.CPU, err = getAvailableResourceQuantity(defaultResources.CPU, k8sv1.ResourceLimitsCPU, quota) if err != nil { return nil, errors.Wrap(err, "get available resource quantity") } - + // Memory availableResources.Memory, err = getAvailableResourceQuantity(defaultResources.Memory, k8sv1.ResourceLimitsMemory, quota) if err != nil { return nil, errors.Wrap(err, "get available resource quantity") } - + // Ephemeral Storage availableResources.EphemeralStorage, err = getAvailableResourceQuantity(defaultResources.EphemeralStorage, k8sv1.ResourceLimitsEphemeralStorage, quota) if err != nil { return nil, errors.Wrap(err, "get available resource quantity") } - + // Get limitrange limitrange, err := ctx.KubeClient().KubeClient().CoreV1().LimitRanges(b.BuildNamespace).Get(ctx.Context(), devspaceLimitRange, metav1.GetOptions{}) if err != nil { return availableResources, nil } - + // Check if container limit is smaller than the available resources for _, limit := range limitrange.Spec.Limits { if limit.Type == k8sv1.LimitTypeContainer { @@ -422,7 +422,7 @@ func (b *Builder) getAvailableResources(ctx devspacecontext.Context) (*available } } } - + return availableResources, nil } @@ -432,17 +432,17 @@ func getAvailableResourceQuantity(defaultQuantity resource.Quantity, resourceNam retLimit = quotaLimit if quotaUsed, ok := quota.Status.Used[resourceName]; ok { retLimit.Sub(quotaUsed) - + if retLimit.Cmp(defaultQuantity) == 1 { retLimit = defaultQuantity } } } - + // Check if limit == 0 or below zero if retLimit.Sign() != 1 { return resource.MustParse("0"), errors.Errorf("Available %s resource is zero or below zero: %s", resourceName, retLimit.String()) } - + return retLimit, nil } diff --git a/pkg/devspace/build/builder/kaniko/kaniko.go b/pkg/devspace/build/builder/kaniko/kaniko.go index 5034e7a0d8..b9776ac7db 100644 --- a/pkg/devspace/build/builder/kaniko/kaniko.go +++ b/pkg/devspace/build/builder/kaniko/kaniko.go @@ -5,21 +5,21 @@ import ( "fmt" "io" "strings" - + devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" "github.com/loft-sh/devspace/pkg/devspace/kubectl/selector" "github.com/loft-sh/devspace/pkg/devspace/services/logs" "github.com/sirupsen/logrus" - + "github.com/loft-sh/devspace/pkg/util/interrupt" "github.com/loft-sh/devspace/pkg/util/progressreader" - + "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/exec" - + "github.com/loft-sh/devspace/pkg/devspace/build/builder" "github.com/loft-sh/devspace/pkg/devspace/build/builder/helper" "github.com/loft-sh/devspace/pkg/devspace/build/builder/restart" @@ -28,16 +28,16 @@ import ( "github.com/loft-sh/devspace/pkg/devspace/services/targetselector" logpkg "github.com/loft-sh/devspace/pkg/util/log" "github.com/loft-sh/devspace/pkg/util/randutil" - + "os" "path/filepath" "time" - + "github.com/docker/cli/cli/command/image/build" + buildtypes "github.com/docker/docker/api/types/build" dockerterm "github.com/moby/term" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - buildtypes "github.com/docker/docker/api/types/build" ) // EngineName is the name of the building engine @@ -50,11 +50,11 @@ var ( // Builder holds the necessary information to build and push docker images type Builder struct { helper *helper.BuildHelper - + PullSecretName string FullImageName string BuildNamespace string - + allowInsecureRegistry bool } @@ -69,31 +69,31 @@ func NewBuilder(ctx devspacecontext.Context, imageConf *latest.Image, imageTags return nil, err } } - + buildNamespace := ctx.KubeClient().Namespace() if imageConf.Kaniko.Namespace != "" { buildNamespace = imageConf.Kaniko.Namespace } - + allowInsecurePush := false if imageConf.Kaniko.Insecure != nil { allowInsecurePush = *imageConf.Kaniko.Insecure } - + pullSecretName := "" if imageConf.Kaniko.PullSecret != "" { pullSecretName = imageConf.Kaniko.PullSecret } - + b := &Builder{ PullSecretName: pullSecretName, FullImageName: imageConf.Image + ":" + imageTags[0], BuildNamespace: buildNamespace, - + allowInsecureRegistry: allowInsecurePush, helper: helper.NewBuildHelper(ctx, EngineName, imageConf, imageTags), } - + return b, nil } @@ -110,12 +110,12 @@ func (b *Builder) ShouldRebuild(ctx devspacecontext.Context, forceRebuild bool) // BuildImage builds a dockerimage within a kaniko pod func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfilePath string, entrypoint []string, cmd []string) error { var err error - + contextPath, err = build.ResolveAndValidateContextPath(contextPath) if err != nil { return errors.Wrap(err, "resolve context path") } - + // build options options := &buildtypes.ImageBuildOptions{} if b.helper.ImageConf.BuildArgs != nil { @@ -127,7 +127,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if b.helper.ImageConf.Network != "" { options.NetworkMode = b.helper.ImageConf.Network } - + // Check if we should overwrite entrypoint injectRestartHelper := b.helper.ImageConf.InjectRestartHelper || b.helper.ImageConf.InjectLegacyRestartHelper if len(entrypoint) > 0 || len(cmd) > 0 || injectRestartHelper || len(b.helper.ImageConf.AppendDockerfileInstructions) > 0 { @@ -135,10 +135,10 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if err != nil { return err } - + defer os.RemoveAll(filepath.Dir(dockerfilePath)) } - + // Generate the build pod spec randString := randutil.GenerateRandomString(12) buildID := strings.ToLower(randString) @@ -146,29 +146,29 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if err != nil { return errors.Wrap(err, "get build pod") } - + // Delete the build pod when we are done or get interrupted during build deleteBuildPod := func() { gracePeriod := int64(3) if buildPod.Name == "" { return } - + deleteErr := ctx.KubeClient().KubeClient().CoreV1().Pods(b.BuildNamespace).Delete(ctx.Context(), buildPod.Name, metav1.DeleteOptions{ GracePeriodSeconds: &gracePeriod, }) - + if deleteErr != nil { ctx.Log().Errorf("Failed to delete build pod: %s", deleteErr.Error()) } } - + err = interrupt.Global.RunAlways(func() error { buildPodCreated, err := ctx.KubeClient().KubeClient().CoreV1().Pods(b.BuildNamespace).Create(ctx.Context(), buildPod, metav1.CreateOptions{}) if err != nil { return errors.Errorf("unable to create build pod: %s", err.Error()) } - + ctx.Log().Info("Waiting for build init container to start...") err = wait.PollUntilContextTimeout(ctx.Context(), time.Second, waitTimeout, true, func(ctxPollUntil context.Context) (done bool, err error) { buildPod, err = ctx.KubeClient().KubeClient().CoreV1().Pods(b.BuildNamespace).Get(ctxPollUntil, buildPodCreated.Name, metav1.GetOptions{}) @@ -176,7 +176,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if kerrors.IsNotFound(err) { return false, nil } - + return false, err } else if len(buildPod.Status.InitContainerStatuses) > 0 { status := buildPod.Status.InitContainerStatuses[0] @@ -192,7 +192,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if errorLog == "" { errorLog = buildPod.Status.InitContainerStatuses[0].State.Terminated.Message } - + return false, fmt.Errorf("kaniko init container %s/%s has unexpectedly exited with code %d: %s", buildPod.Namespace, buildPod.Name, buildPod.Status.InitContainerStatuses[0].State.Terminated.ExitCode, errorLog) } else if status.State.Waiting != nil { if kubectl.CriticalStatus[status.State.Waiting.Reason] { @@ -200,13 +200,13 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil } } } - + return len(buildPod.Status.InitContainerStatuses) > 0 && buildPod.Status.InitContainerStatuses[0].State.Running != nil, nil }) if err != nil { return errors.Wrap(err, "waiting for kaniko init") } - + // Get ignore rules from docker ignore relDockerfile, err := archive.CanonicalTarNameForPath(dockerfilePath) if err != nil { @@ -219,7 +219,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if err := build.ValidateContextDirectory(contextPath, ignoreRules); err != nil { return errors.Errorf("error checking context: '%s'", err) } - + ctx.Log().Info("Uploading files to build container...") buildCtx, err := archive.TarWithOptions(contextPath, &archive.TarOptions{ ExcludePatterns: ignoreRules, @@ -228,38 +228,38 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if err != nil { return err } - + // Wrap it with our custom io.ReadCloser in order to show progress. buildCtx = &progressreader.ProgressReader{ReadCloser: buildCtx, Ctx: ctx} - + // Copy complete context _, stderr, err := ctx.KubeClient().ExecBuffered(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, []string{"tar", "xp", "-C", kanikoContextPath + "/."}, buildCtx) if err != nil { if stderr != nil { return errors.Errorf("copy context: error executing tar: %s: %v", string(stderr), err) } - + return errors.Wrap(err, "copy context") } - + // Copy dockerfile err = ctx.KubeClient().Copy(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, kanikoContextPath, dockerfilePath, []string{}) if err != nil { return errors.Errorf("error uploading dockerfile to container: %v", err) } - + // Copy restart helper script if injectRestartHelper { tempDir, err := os.MkdirTemp("", "") if err != nil { return err } - + defer os.RemoveAll(tempDir) - + scriptPath := filepath.Join(tempDir, restart.ScriptName) remoteFolder := filepath.ToSlash(filepath.Join(kanikoContextPath, ".devspace", ".devspace")) - + var helperScript string if b.helper.ImageConf.InjectRestartHelper { helperScript, err = restart.LoadRestartHelper(b.helper.ImageConf.RestartHelperPath) @@ -272,30 +272,30 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil return errors.Wrap(err, "load legacy restart helper") } } - + err = os.WriteFile(scriptPath, []byte(helperScript), 0777) if err != nil { return errors.Wrap(err, "write restart helper script") } - + // create the .devspace directory in the container _, _, err = ctx.KubeClient().ExecBuffered(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, []string{"mkdir", "-p", remoteFolder}, nil) if err != nil { return errors.Errorf("error executing command 'mkdir -p %s' in init container: %v", remoteFolder, err) } - + // copy the helper script into the container err = ctx.KubeClient().Copy(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, remoteFolder, scriptPath, []string{}) if err != nil { return errors.Errorf("error uploading helper script to container: %v", err) } - + // change permissions for the execution script _, _, err = ctx.KubeClient().ExecBuffered(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, []string{"chmod", "-R", "0777", remoteFolder}, nil) if err != nil { return errors.Errorf("error executing command 'chmod +x %s' in init container: %v", filepath.Join(kanikoContextPath, restart.ScriptName), err) } - + // remove the .dockerignore since .devspace is usually ignored and we want to sneak our helper script in // this shouldn't be any issue since the context was already pruned in the copy step beforehand _, _, err = ctx.KubeClient().ExecBuffered(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, []string{"rm", filepath.ToSlash(filepath.Join(kanikoContextPath, ".dockerignore"))}, nil) @@ -305,13 +305,13 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil } } } - + // Tell init container we are done _, _, err = ctx.KubeClient().ExecBuffered(ctx.Context(), buildPod, buildPod.Spec.InitContainers[0].Name, []string{"touch", doneFile}, nil) if err != nil { return errors.Errorf("Error executing command in init container: %v", err) } - + ctx.Log().Done("Uploaded files to container") ctx.Log().Info("Waiting for kaniko container to start...") err = wait.PollUntilContextTimeout(ctx.Context(), time.Second, waitTimeout, true, func(ctxPollUntil context.Context) (done bool, err error) { @@ -320,7 +320,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if kerrors.IsNotFound(err) { return false, nil } - + return false, err } else if len(buildPod.Status.ContainerStatuses) > 0 { status := buildPod.Status.ContainerStatuses[0] @@ -328,7 +328,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if status.State.Terminated.ExitCode == 0 { return true, nil } - + errorLog := "" reader, _ := ctx.KubeClient().Logs(ctxPollUntil, b.BuildNamespace, buildPodCreated.Name, status.Name, false, nil, false) if reader != nil { @@ -340,7 +340,7 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if errorLog == "" { errorLog = buildPod.Status.ContainerStatuses[0].State.Terminated.Message } - + return false, fmt.Errorf("kaniko pod %s/%s has unexpectedly exited with code %d: %s", buildPod.Namespace, buildPod.Name, status.State.Terminated.ExitCode, errorLog) } else if status.State.Waiting != nil { if kubectl.CriticalStatus[status.State.Waiting.Reason] { @@ -348,15 +348,15 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil } } } - + return len(buildPod.Status.ContainerStatuses) > 0 && buildPod.Status.ContainerStatuses[0].Ready, nil }) if err != nil { return errors.Wrap(err, "waiting for kaniko") } - + ctx.Log().Done("Build pod has started") - + // Determine output writer var writer io.WriteCloser if ctx.Log() == logpkg.GetInstance() { @@ -365,9 +365,9 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil writer = ctx.Log().Writer(logrus.InfoLevel, false) } defer writer.Close() - + stdoutLogger := kanikoLogger{out: writer} - + // Stream the logs options := targetselector.NewOptionsFromFlags(buildPod.Spec.Containers[0].Name, "", nil, buildPod.Namespace, buildPod.Name). WithWait(false). @@ -376,23 +376,23 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil if err != nil { return errors.Errorf("error printing build logs: %v", err) } - + ctx.Log().Info("Checking build status...") for { time.Sleep(time.Second) - + // Check if build was successful pod, err := ctx.KubeClient().KubeClient().CoreV1().Pods(b.BuildNamespace).Get(ctx.Context(), buildPodCreated.Name, metav1.GetOptions{}) if err != nil { return errors.Errorf("Error checking if build was successful: %v", err) } - + // Check if terminated if len(pod.Status.ContainerStatuses) > 0 && pod.Status.ContainerStatuses[0].State.Terminated != nil { if pod.Status.ContainerStatuses[0].State.Terminated.ExitCode != 0 { return errors.Errorf("error building image (Exit Code %d)", pod.Status.ContainerStatuses[0].State.Terminated.ExitCode) } - + break } } @@ -411,9 +411,9 @@ func (b *Builder) BuildImage(ctx devspacecontext.Context, contextPath, dockerfil for _, pod := range pods.Items { _ = ctx.KubeClient().KubeClient().CoreV1().Pods(b.BuildNamespace).Delete(ctx.Context(), pod.Name, metav1.DeleteOptions{}) } - + return err } - + return nil } diff --git a/pkg/devspace/build/builder/localregistry/build.go b/pkg/devspace/build/builder/localregistry/build.go index 394cd824c0..968df37b8f 100644 --- a/pkg/devspace/build/builder/localregistry/build.go +++ b/pkg/devspace/build/builder/localregistry/build.go @@ -6,7 +6,7 @@ import ( "io" "net" "strings" - + "github.com/docker/docker/pkg/jsonmessage" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -16,12 +16,12 @@ import ( devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" dockerclient "github.com/loft-sh/devspace/pkg/devspace/docker" "github.com/pkg/errors" - + + "github.com/docker/docker/api/types/build" buildkit "github.com/moby/buildkit/client" "github.com/moby/buildkit/session" "github.com/moby/buildkit/session/auth/authprovider" "github.com/moby/buildkit/session/upload/uploadprovider" - "github.com/docker/docker/api/types/build" ) func RemoteBuild(ctx devspacecontext.Context, podName, namespace string, buildContext io.Reader, writer io.Writer, buildOptions *build.ImageBuildOptions) error { @@ -29,7 +29,7 @@ func RemoteBuild(ctx devspacecontext.Context, podName, namespace string, buildCo if err != nil { return errors.Wrap(err, "connect to buildkit pod") } - + // create new buildkit client client, err := buildkit.New(ctx.Context(), "", buildkit.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { return conn, nil @@ -38,12 +38,12 @@ func RemoteBuild(ctx devspacecontext.Context, podName, namespace string, buildCo return err } defer client.Close() - + dockerConfig, err := dockerclient.LoadDockerConfig() if err != nil { return err } - + // stdin is context up := uploadprovider.New() options := buildkit.SolveOpt{ @@ -65,24 +65,24 @@ func RemoteBuild(ctx devspacecontext.Context, podName, namespace string, buildCo }, }, } - + for key, value := range buildOptions.BuildArgs { if value == nil { continue } options.FrontendAttrs["build-arg:"+key] = *value } - + pw, err := NewPrinter(context.TODO(), writer) if err != nil { return err } - + _, err = client.Solve(ctx.Context(), nil, options, pw.Status()) if err != nil { return err } - + return nil } @@ -96,36 +96,36 @@ func LocalBuild(ctx devspacecontext.Context, contextPath, dockerfilePath string, if err != nil { return err } - + dockerClient, err := dockerclient.NewClient(ctx.Context(), ctx.Log()) if err != nil { return nil } - + // make sure to use the correct proxy configuration buildOptions.BuildArgs = dockerClient.ParseProxyConfig(buildOptions.BuildArgs) - + response, err := dockerClient.ImageBuild(ctx.Context(), body, *buildOptions) if err != nil { return err } defer response.Body.Close() - + err = jsonmessage.DisplayJSONMessagesStream(response.Body, outStream, outStream.FD(), outStream.IsTerminal(), nil) if err != nil { return err } - + for _, tag := range buildOptions.Tags { ctx.Log().Info("The push refers to repository [" + tag + "]") err := CopyImageToRemote(ctx.Context(), dockerClient, tag, writer, b) if err != nil { return errors.Errorf("error during local registry image push: %v", err) } - + ctx.Log().Info("Image pushed to local registry") } - + return nil } @@ -146,7 +146,7 @@ func CopyImageToRemote(ctx context.Context, client dockerclient.Client, imageNam if err != nil { return err } - + progressChan := make(chan v1.Update, 200) errChan := make(chan error, 1) // push image to remote registry @@ -158,17 +158,17 @@ func CopyImageToRemote(ctx context.Context, client dockerclient.Client, imageNam remote.WithProgress(progressChan), ) }() - + for update := range progressChan { if update.Error != nil { return update.Error } - + status := "Pushing" if update.Complete == update.Total { status = "Pushed" } - + jm := &jsonmessage.JSONMessage{ ID: localRef.Identifier(), Status: status, @@ -177,12 +177,12 @@ func CopyImageToRemote(ctx context.Context, client dockerclient.Client, imageNam Total: update.Total, }, } - + _, err := fmt.Fprintf(writer, "%s %s\n", jm.Status, jm.Progress.String()) if err != nil { return err } } - + return <-errChan } diff --git a/pkg/devspace/configure/image.go b/pkg/devspace/configure/image.go index 145e86fe8e..cb758a4a58 100644 --- a/pkg/devspace/configure/image.go +++ b/pkg/devspace/configure/image.go @@ -10,11 +10,11 @@ import ( "path" "regexp" "strings" - + "github.com/loft-sh/devspace/pkg/util/encoding" "github.com/loft-sh/utils/pkg/command" "github.com/sirupsen/logrus" - + "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" "github.com/loft-sh/devspace/pkg/devspace/docker" "github.com/loft-sh/devspace/pkg/devspace/generator" @@ -41,19 +41,19 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string skip = "Skip / I don't know" err error ) - + imageConfig := &latest.Image{ Image: strings.ToLower(image), Dockerfile: dockerfile, } - + buildMethods := []string{subPathDockerfile} - + stat, err := os.Stat(imageConfig.Dockerfile) if err == nil && !stat.IsDir() { buildMethods = []string{rootLevelDockerfile, differentDockerfile} } - + buildMethod, err := m.log.Question(&survey.QuestionOptions{ Question: "How should DevSpace build the container image for this project?", DefaultValue: buildMethods[0], @@ -62,7 +62,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string if err != nil { return err } - + if buildMethod == customBuild { buildCommand, err := m.log.Question(&survey.QuestionOptions{ Question: "Please enter your build command without the image (e.g. `gradle jib --image` => DevSpace will append the image name automatically)", @@ -70,7 +70,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string if err != nil { return err } - + imageConfig.Custom = &latest.CustomConfig{ Command: buildCommand + " --tag=$(get_image --only=tag " + imageName + ")", } @@ -82,7 +82,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string if value == "" { return nil } - + stat, err := os.Stat(value) if err == nil && !stat.IsDir() { return nil @@ -93,7 +93,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string if err != nil { return err } - + if imageConfig.Dockerfile != "" { imageConfig.Context, err = m.log.Question(&survey.QuestionOptions{ Question: "What is the build context for building this image?", @@ -114,19 +114,19 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string } } } - + if image == "" && buildMethod != skip { kubeClient, err := kubectl.NewDefaultClient() if err != nil { return err } - + // Get docker client dockerClient, err := m.factory.NewDockerClientWithMinikube(context.TODO(), kubeClient, true, m.log) if err != nil { return errors.Errorf("Cannot create docker client: %v", err) } - + // Check if user is logged into docker hub isLoggedIntoDockerHub := false authConfig, err := dockerClient.GetAuthConfig(context.TODO(), dockerHubHostname, true) @@ -134,7 +134,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string useDockerHub = useDockerHub + fmt.Sprintf(registryUsernameHint, authConfig.Username) isLoggedIntoDockerHub = true } - + // Check if user is logged into GitHub isLoggedIntoGitHub := false authConfig, err = dockerClient.GetAuthConfig(context.TODO(), generator.GithubContainerRegistry, true) @@ -142,7 +142,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string useGithubRegistry = useGithubRegistry + fmt.Sprintf(registryUsernameHint, authConfig.Username) isLoggedIntoGitHub = true } - + // Set registry select options according to logged in status of dockerhub and github registryOptions := []string{skipRegistry, useDockerHub, useGithubRegistry, useOtherRegistry} if isLoggedIntoGitHub { @@ -152,7 +152,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string registryDefaultOption = useDockerHub registryOptions = []string{useDockerHub, useGithubRegistry, useOtherRegistry, skipRegistry} } - + selectedRegistry, err := m.log.Question(&survey.QuestionOptions{ Question: "If you were to push any images, which container registry would you want to push to?", DefaultValue: registryDefaultOption, @@ -161,7 +161,7 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string if err != nil { return err } - + if selectedRegistry == skipRegistry { imageConfig.Image = "my-image-registry.tld/username" + "/" + imageName } else { @@ -181,23 +181,23 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string return err } } - + registryUsername, err := m.addPullSecretConfig(dockerClient, strings.Trim(registryHostname+"/username/app", "/")) if err != nil { return err } - + if registryUsername == "" { registryUsername = "username" } - + if selectedRegistry == useDockerHub { imageConfig.Image = registryUsername + "/" + imageName } else { if projectNamespace == "" { projectNamespace = registryUsername } - + if regexp.MustCompile(`^(.+\.)?gcr.io$`).Match([]byte(registryHostname)) { projectNamespace = "project" project, err := command.Output(context.TODO(), "", expand.ListEnviron(os.Environ()...), "gcloud", "config", "get-value", "project") @@ -205,17 +205,17 @@ func (m *manager) AddImage(imageName, image, projectNamespace, dockerfile string projectNamespace = strings.TrimSpace(string(project)) } } - + imageConfig.Image = registryHostname + "/" + projectNamespace + "/" + imageName } } } - + if buildMethod == skip { imageConfig.Image = "username/app" imageConfig.Dockerfile = "./Dockerfile" } - + m.config.Images[imageName] = imageConfig return nil } @@ -226,50 +226,50 @@ func (m *manager) addPullSecretConfig(dockerClient docker.Client, image string) if err != nil { return "", err } - + registryHostname, err := pullsecrets.GetRegistryFromImageName(image) if err != nil { return "", err } - + registryHostnamePrintable := registryHostname if registryHostnamePrintable == "" { registryHostnamePrintable = dockerHubHostname } - + usernameQuestion := fmt.Sprintf("What is your username for %s? (optional, Enter to skip)", registryHostnamePrintable) passwordQuestion := fmt.Sprintf("What is your password for %s? (optional, Enter to skip)", registryHostnamePrintable) if strings.Contains(registryHostname, "ghcr.io") || strings.Contains(registryHostname, "github.com") { usernameQuestion = "What is your GitHub username? (optional, Enter to skip)" passwordQuestion = "Please enter a GitHub personal access token (optional, Enter to skip)" } - + registryUsername := "" registryPassword := "" retry := false - + m.log.WriteString(logrus.WarnLevel, "\n") - + for { m.log.Info("Checking registry authentication for " + registryHostnamePrintable + "...") authConfig, err := dockerClient.Login(context.TODO(), registryHostname, registryUsername, registryPassword, true, retry, retry) if err == nil && (authConfig.Username != "" || authConfig.Password != "") { registryUsername = authConfig.Username - + m.log.Donef("Great! You are authenticated with %s", registryHostnamePrintable) break } - + m.log.WriteString(logrus.WarnLevel, "\n") m.log.Warnf("Unable to find registry credentials for %s", registryHostnamePrintable) m.log.Warnf("Running `%s` for you to authenticate with the registry (optional)", strings.TrimSpace("docker login "+registryHostname)) - + registryUsername, err = m.log.Question(&survey.QuestionOptions{ Question: usernameQuestion, ValidationRegexPattern: "^[^A-Z\\s]+\\.[^A-Z\\s]+$", ValidationMessage: "Error parsing registry username: must only include lowercase letters.", }) - + if err != nil { return "", err } @@ -283,24 +283,24 @@ func (m *manager) addPullSecretConfig(dockerClient docker.Client, image string) return "", err } } - + m.log.WriteString(logrus.WarnLevel, "\n") - + // Check if docker is running _, runErr := command.Output(context.TODO(), "", expand.ListEnviron(os.Environ()...), "docker", "version") - + // If Docker is available, ask if we should retry registry login if runErr == nil && registryUsername != "" { retry = true } - + if !retry { m.log.Warn("Skip validating image registry credentials.") m.log.Warn("You may ignore this warning. Pushing images to a registry is *not* required.") - + usernameVar := "REGISTRY_USERNAME" passwordVar := "REGISTRY_PASSWORD" - + m.config.PullSecrets = map[string]*latest.PullSecretConfig{ encoding.Convert(registryHostname): { Registry: registryHostname, @@ -308,7 +308,7 @@ func (m *manager) addPullSecretConfig(dockerClient docker.Client, image string) Password: fmt.Sprintf("${%s}", passwordVar), }, } - + if m.config.Vars == nil { m.config.Vars = map[string]*latest.Variable{} } @@ -316,13 +316,13 @@ func (m *manager) addPullSecretConfig(dockerClient docker.Client, image string) Name: passwordVar, Password: true, } - + m.localCache.SetVar(usernameVar, registryUsername) m.localCache.SetVar(passwordVar, registryPassword) - + break } } - + return registryUsername, nil } diff --git a/pkg/devspace/docker/auth_test.go b/pkg/devspace/docker/auth_test.go index ef7d8c362e..e7f0937231 100644 --- a/pkg/devspace/docker/auth_test.go +++ b/pkg/devspace/docker/auth_test.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" "testing" - + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" dockerregistry "github.com/docker/docker/api/types/registry" @@ -14,7 +14,7 @@ import ( dockerclient "github.com/docker/docker/client" "github.com/loft-sh/devspace/pkg/util/fsutil" "gopkg.in/yaml.v3" - + "gotest.tools/assert" ) @@ -61,9 +61,9 @@ func (f *fakeDockerClient) ImageRemove(ctx context.Context, img string, options type getRegistryEndpointTestCase struct { name string - + registryURL string - + expectedIsDefault bool expectedEndpoint string expectedErr bool @@ -83,20 +83,20 @@ func TestGetRegistryEndpoint(t *testing.T) { expectedEndpoint: "custom", }, } - + for _, testCase := range testCases { client := &client{ APIClient: &fakeDockerClient{}, } - + isDefault, endpoint, err := client.GetRegistryEndpoint(context.Background(), testCase.registryURL) - + if !testCase.expectedErr { assert.NilError(t, err, "Unexpected error in testCase %s", testCase.name) } else if err == nil { t.Fatalf("Unexpected error %v in testCase %s", err, testCase.name) } - + assert.Equal(t, isDefault, testCase.expectedIsDefault, "Unexpected isDefault bool in testCase %s", testCase.name) assert.Equal(t, endpoint, testCase.expectedEndpoint, "Unexpected endpoint in testCase %s", testCase.name) } @@ -104,11 +104,11 @@ func TestGetRegistryEndpoint(t *testing.T) { type getAuthConfigTestCase struct { name string - + files map[string]interface{} registryURL string checkCredentialsStore bool - + expectedAuthConfig *dockerregistry.AuthConfig expectedErr bool } @@ -131,9 +131,9 @@ func TestGetAuthConfig(t *testing.T) { }, }, } - + dir := t.TempDir() - + wdBackup, err := os.Getwd() if err != nil { t.Fatalf("Error getting current working directory: %v", err) @@ -146,16 +146,16 @@ func TestGetAuthConfig(t *testing.T) { if err != nil { t.Fatal(err) } - + defer func() { err = os.Chdir(wdBackup) if err != nil { t.Fatalf("Error changing dir back: %v", err) } }() - + configDir = dir - + for _, testCase := range testCases { for path, content := range testCase.files { asJSON, err := json.Marshal(content) @@ -166,25 +166,25 @@ func TestGetAuthConfig(t *testing.T) { err = fsutil.WriteToFile(asJSON, path) assert.NilError(t, err, "Error writing file in testCase %s", testCase.name) } - + client := &client{ APIClient: &fakeDockerClient{}, } - + auth, err := client.GetAuthConfig(context.Background(), testCase.registryURL, testCase.checkCredentialsStore) - + if !testCase.expectedErr { assert.NilError(t, err, "Unexpected error in testCase %s", testCase.name) } else if err == nil { t.Fatalf("Unexpected error %v in testCase %s", err, testCase.name) } - + authAsYaml, err := yaml.Marshal(auth) assert.NilError(t, err, "Error parsing authConfig to yaml in testCase %s", testCase.name) expectedAsYaml, err := yaml.Marshal(testCase.expectedAuthConfig) assert.NilError(t, err, "Error parsing exception to yaml in testCase %s", testCase.name) assert.Equal(t, string(authAsYaml), string(expectedAsYaml), "Unexpected authConfig in testCase %s", testCase.name) - + err = filepath.Walk(".", func(path string, f os.FileInfo, err error) error { os.RemoveAll(path) return nil @@ -195,7 +195,7 @@ func TestGetAuthConfig(t *testing.T) { type loginTestCase struct { name string - + files map[string]interface{} registryURL string user string @@ -203,7 +203,7 @@ type loginTestCase struct { checkCredentialsStore bool saveAuthConfig bool relogin bool - + expectedAuthConfig *dockerregistry.AuthConfig expectedErr bool } @@ -223,9 +223,9 @@ func TestLogin(t *testing.T) { }, }, } - + dir := t.TempDir() - + wdBackup, err := os.Getwd() if err != nil { t.Fatalf("Error getting current working directory: %v", err) @@ -238,16 +238,16 @@ func TestLogin(t *testing.T) { if err != nil { t.Fatal(err) } - + defer func() { err = os.Chdir(wdBackup) if err != nil { t.Fatalf("Error changing dir back: %v", err) } }() - + configDir = dir - + for _, testCase := range testCases { for path, content := range testCase.files { asJSON, err := json.Marshal(content) @@ -258,24 +258,24 @@ func TestLogin(t *testing.T) { err = fsutil.WriteToFile(asJSON, path) assert.NilError(t, err, "Error writing file in testCase %s", testCase.name) } - + client := &client{ APIClient: &fakeDockerClient{}, } - + auth, err := client.Login(context.Background(), testCase.registryURL, testCase.user, testCase.password, testCase.checkCredentialsStore, testCase.saveAuthConfig, testCase.relogin) if !testCase.expectedErr { assert.NilError(t, err, "Unexpected error in testCase %s", testCase.name) } else if err == nil { t.Fatalf("Unexpected error %v in testCase %s", err, testCase.name) } - + authAsYaml, err := yaml.Marshal(auth) assert.NilError(t, err, "Error parsing authConfig to yaml in testCase %s", testCase.name) expectedAsYaml, err := yaml.Marshal(testCase.expectedAuthConfig) assert.NilError(t, err, "Error parsing exception to yaml in testCase %s", testCase.name) assert.Equal(t, string(authAsYaml), string(expectedAsYaml), "Unexpected authConfig in testCase %s", testCase.name) - + err = filepath.Walk(".", func(path string, f os.FileInfo, err error) error { os.RemoveAll(path) return nil diff --git a/pkg/devspace/docker/cli.go b/pkg/devspace/docker/cli.go index 1073d65ed5..100f983a97 100644 --- a/pkg/devspace/docker/cli.go +++ b/pkg/devspace/docker/cli.go @@ -7,9 +7,9 @@ import ( "io" "mvdan.cc/sh/v3/expand" "strings" - + "github.com/loft-sh/devspace/pkg/util/log" - + "github.com/docker/docker/api/types/build" ) @@ -21,7 +21,7 @@ func (c *client) ImageBuildCLI(ctx context.Context, workingDir string, environ e if v == nil { continue } - + args = append(args, "--build-arg", k+"="+*v) } } @@ -31,19 +31,19 @@ func (c *client) ImageBuildCLI(ctx context.Context, workingDir string, environ e for _, tag := range options.Tags { args = append(args, "--tag", tag) } - + if options.Dockerfile != "" { args = append(args, "--file", options.Dockerfile) } if options.Target != "" { args = append(args, "--target", options.Target) } - + args = append(args, additionalArgs...) args = append(args, "-") - + log.Infof("Execute docker cli command with: docker %s", strings.Join(args, " ")) - + extraEnv := map[string]string{} if useBuildKit { extraEnv["DOCKER_BUILDKIT"] = "1" @@ -53,7 +53,7 @@ func (c *client) ImageBuildCLI(ctx context.Context, workingDir string, environ e extraEnv[k] = v } } - + environ = env.NewVariableEnvProvider(environ, extraEnv) return command.Command(ctx, workingDir, environ, writer, writer, context, "docker", args...) } diff --git a/pkg/devspace/docker/client.go b/pkg/devspace/docker/client.go index 7685e5ef61..ac2970397c 100644 --- a/pkg/devspace/docker/client.go +++ b/pkg/devspace/docker/client.go @@ -8,21 +8,21 @@ import ( "os/exec" "path/filepath" "strings" - + "github.com/docker/docker/api/types/image" dockerregistry "github.com/docker/docker/api/types/registry" "github.com/loft-sh/utils/pkg/command" "mvdan.cc/sh/v3/expand" - + "github.com/loft-sh/devspace/pkg/devspace/kubectl" "github.com/loft-sh/devspace/pkg/util/log" - + dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/filters" dockerclient "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" "github.com/pkg/errors" - "github.com/docker/docker/api/types/build" ) var errNotMinikube = errors.New("not a minikube context") @@ -31,17 +31,17 @@ var errNotMinikube = errors.New("not a minikube context") type Client interface { Ping(ctx context.Context) (dockertypes.Ping, error) NegotiateAPIVersion(ctx context.Context) - + ImageBuild(ctx context.Context, context io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) ImageBuildCLI(ctx context.Context, workingDir string, environ expand.Environ, useBuildkit bool, context io.Reader, writer io.Writer, additionalArgs []string, options build.ImageBuildOptions, log log.Logger) error - + ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) - + Login(ctx context.Context, registryURL, user, password string, checkCredentialsStore, saveAuthConfig, relogin bool) (*dockerregistry.AuthConfig, error) GetAuthConfig(ctx context.Context, registryURL string, checkCredentialsStore bool) (*dockerregistry.AuthConfig, error) - + ParseProxyConfig(buildArgs map[string]*string) map[string]*string - + DeleteImageByName(ctx context.Context, imageName string, log log.Logger) ([]image.DeleteResponse, error) DeleteImageByFilter(ctx context.Context, filter filters.Args, log log.Logger) ([]image.DeleteResponse, error) DockerAPIClient() dockerclient.APIClient @@ -50,7 +50,7 @@ type Client interface { // Client is a client for docker type client struct { dockerclient.APIClient - + minikubeEnv map[string]string } @@ -63,7 +63,7 @@ func NewClient(ctx context.Context, log log.Logger) (Client, error) { func NewClientWithMinikube(ctx context.Context, kubectlClient kubectl.Client, preferMinikube bool, log log.Logger) (Client, error) { var cli Client var err error - + if preferMinikube { cli, err = newDockerClientFromMinikube(ctx, kubectlClient) if err != nil && err != errNotMinikube { @@ -74,7 +74,7 @@ func NewClientWithMinikube(ctx context.Context, kubectlClient kubectl.Client, pr cli, err = newDockerClientFromEnvironment() if err != nil { log.Warnf("Error creating docker client from environment: %v", err) - + // Last try to create it without the environment option cli, err = newDockerClient() if err != nil { @@ -82,7 +82,7 @@ func NewClientWithMinikube(ctx context.Context, kubectlClient kubectl.Client, pr } } } - + cli.NegotiateAPIVersion(ctx) return cli, nil } @@ -92,7 +92,7 @@ func newDockerClient() (Client, error) { if err != nil { return nil, errors.Errorf("Couldn't create docker client: %s", err) } - + return &client{ APIClient: cli, minikubeEnv: nil, @@ -104,7 +104,7 @@ func newDockerClientFromEnvironment() (Client, error) { if err != nil { return nil, errors.Errorf("Couldn't create docker client: %s", err) } - + return &client{ APIClient: cli, minikubeEnv: nil, @@ -115,12 +115,12 @@ func newDockerClientFromMinikube(ctx context.Context, kubectlClient kubectl.Clie if !kubectl.IsMinikubeKubernetes(kubectlClient) { return nil, errNotMinikube } - + env, err := GetMinikubeEnvironment(ctx, kubectlClient.CurrentContext()) if err != nil { return nil, errors.Errorf("can't retrieve minikube docker environment due to error: %v", err) } - + var httpclient *http.Client if dockerCertPath := env["DOCKER_CERT_PATH"]; dockerCertPath != "" { options := tlsconfig.Options{ @@ -133,7 +133,7 @@ func newDockerClientFromMinikube(ctx context.Context, kubectlClient kubectl.Clie if err != nil { return nil, err } - + httpclient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsc, @@ -141,17 +141,17 @@ func newDockerClientFromMinikube(ctx context.Context, kubectlClient kubectl.Clie CheckRedirect: dockerclient.CheckRedirect, } } - + host := env["DOCKER_HOST"] if host == "" { host = dockerclient.DefaultDockerHost } - + cli, err := dockerclient.NewClientWithOpts(dockerclient.WithHost(host), dockerclient.WithVersion(env["DOCKER_API_VERSION"]), dockerclient.WithHTTPClient(httpclient), dockerclient.WithHTTPHeaders(nil)) if err != nil { return nil, err } - + return &client{ APIClient: cli, minikubeEnv: env, @@ -166,18 +166,18 @@ func GetMinikubeEnvironment(ctx context.Context, kubeContext string) (map[string } return nil, errors.Errorf("error executing 'minikube docker-env --shell none'\nerror: %v\noutput: %s", err, string(out)) } - + env := map[string]string{} for _, line := range strings.Split(string(out), "\n") { envKeyValue := strings.Split(line, "=") - + if len(envKeyValue) != 2 { continue } - + env[envKeyValue[0]] = envKeyValue[1] } - + return env, nil } @@ -191,6 +191,6 @@ func (c *client) ParseProxyConfig(buildArgs map[string]*string) map[string]*stri if err == nil { buildArgs = dockerConfig.ParseProxyConfig(c.DaemonHost(), buildArgs) } - + return buildArgs } diff --git a/pkg/devspace/docker/images_test.go b/pkg/devspace/docker/images_test.go index 38c4f1ae38..0778ab8103 100644 --- a/pkg/devspace/docker/images_test.go +++ b/pkg/devspace/docker/images_test.go @@ -3,7 +3,7 @@ package docker import ( "context" "testing" - + "github.com/docker/docker/api/types/image" log "github.com/loft-sh/devspace/pkg/util/log/testing" "gopkg.in/yaml.v3" @@ -12,7 +12,7 @@ import ( type deleteImageTestCase struct { name string - + deletedImageName string expectedResponse []image.DeleteResponse expectedErr bool @@ -31,27 +31,27 @@ func TestDeleteImage(t *testing.T) { }, }, } - + for _, testCase := range testCases { var ( response []image.DeleteResponse err error ) - + client := &client{ APIClient: &fakeDockerClient{}, } - + if testCase.deletedImageName != "" { response, err = client.DeleteImageByName(context.Background(), testCase.deletedImageName, &log.FakeLogger{}) } - + if !testCase.expectedErr { assert.NilError(t, err, "Unexpected error in testCase %s", testCase.name) } else if err == nil { t.Fatalf("Unexpected error %v in testCase %s", err, testCase.name) } - + authsAsYaml, err := yaml.Marshal(response) assert.NilError(t, err, "Error parsing response to yaml in testCase %s", testCase.name) expectedAsYaml, err := yaml.Marshal(testCase.expectedResponse) diff --git a/pkg/devspace/docker/testing/fake.go b/pkg/devspace/docker/testing/fake.go index 2e75e6c056..bf5f6bcced 100644 --- a/pkg/devspace/docker/testing/fake.go +++ b/pkg/devspace/docker/testing/fake.go @@ -5,13 +5,13 @@ import ( "context" "io" "strings" - + dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" dockerregistry "github.com/docker/docker/api/types/registry" "github.com/loft-sh/devspace/pkg/util/log" - "github.com/docker/docker/api/types/build" ) // FakeClient is a prototype for a fake docker cient for testing purposes diff --git a/pkg/devspace/helm/downloader/downloader.go b/pkg/devspace/helm/downloader/downloader.go new file mode 100644 index 0000000000..30fda77ef2 --- /dev/null +++ b/pkg/devspace/helm/downloader/downloader.go @@ -0,0 +1,31 @@ +package downloader + +import ( + "context" + "os" + "strings" + + "github.com/loft-sh/utils/pkg/command" + "github.com/loft-sh/utils/pkg/downloader/commands" + "mvdan.cc/sh/v3/expand" +) + +type helmCommand struct { + commands.Command +} + +func NewHelmCommand() commands.Command { + return &helmCommand{ + Command: commands.NewHelmV3Command(), + } +} + +func (h *helmCommand) IsValid(ctx context.Context, path string) (bool, error) { + out, err := command.Output(ctx, "", expand.ListEnviron(os.Environ()...), path, "version") + if err != nil { + return false, nil + } + + outStr := string(out) + return strings.Contains(outStr, `:"v3.`) || strings.Contains(outStr, `:"v4.`), nil +} diff --git a/pkg/devspace/helm/downloader/downloader_test.go b/pkg/devspace/helm/downloader/downloader_test.go new file mode 100644 index 0000000000..8df578cb20 --- /dev/null +++ b/pkg/devspace/helm/downloader/downloader_test.go @@ -0,0 +1,34 @@ +package downloader + +import ( + "context" + "path/filepath" + "runtime" + "testing" +) + +func TestHelmCommandIsValid(t *testing.T) { + // Create a dummy binary to simulate helm output + tmpDir := t.TempDir() + dummyBin := filepath.Join(tmpDir, "dummy-helm") + if runtime.GOOS == "windows" { + dummyBin += ".exe" + } + + // Because we can't easily compile a mock binary in a test without overhead, + // DevSpace tests typically just rely on structural logic or mock interfaces. + // But we can instantiate our command and ensure it doesn't panic on empty input. + cmd := NewHelmCommand() + if cmd.Name() != "helm" { + t.Errorf("expected helm, got %s", cmd.Name()) + } + + // This should fail gracefully because dummyBin doesn't exist + valid, err := cmd.IsValid(context.Background(), dummyBin) + if err != nil { + t.Errorf("expected no error from missing binary, got %v", err) + } + if valid { + t.Error("expected missing binary to be invalid") + } +} diff --git a/pkg/devspace/helm/v3/client.go b/pkg/devspace/helm/v3/client.go index 69b62f326f..5ca332a2aa 100644 --- a/pkg/devspace/helm/v3/client.go +++ b/pkg/devspace/helm/v3/client.go @@ -11,9 +11,9 @@ import ( devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" dependencyutil "github.com/loft-sh/devspace/pkg/devspace/dependency/util" "github.com/loft-sh/devspace/pkg/devspace/helm/generic" + "github.com/loft-sh/devspace/pkg/devspace/helm/downloader" "github.com/loft-sh/devspace/pkg/devspace/helm/types" "github.com/loft-sh/devspace/pkg/util/log" - "github.com/loft-sh/utils/pkg/downloader/commands" "github.com/pkg/errors" "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" @@ -26,7 +26,7 @@ type client struct { // NewClient creates a new helm v3 Client func NewClient(log log.Logger) (types.Client, error) { c := &client{} - c.genericHelm = generic.NewGenericClient(commands.NewHelmV3Command(), log) + c.genericHelm = generic.NewGenericClient(downloader.NewHelmCommand(), log) return c, nil } diff --git a/pkg/devspace/pipeline/engine/basichandler/handler.go b/pkg/devspace/pipeline/engine/basichandler/handler.go index d6437f6de4..9736c64bdf 100644 --- a/pkg/devspace/pipeline/engine/basichandler/handler.go +++ b/pkg/devspace/pipeline/engine/basichandler/handler.go @@ -7,6 +7,7 @@ import ( "time" "github.com/loft-sh/devspace/pkg/devspace/config/constants" + downloaderpkg "github.com/loft-sh/devspace/pkg/devspace/helm/downloader" enginecommands "github.com/loft-sh/devspace/pkg/devspace/pipeline/engine/basichandler/commands" "github.com/loft-sh/devspace/pkg/devspace/pipeline/engine/types" "github.com/loft-sh/devspace/pkg/util/log" @@ -79,7 +80,7 @@ var EnsureCommands = map[string]func(ctx context.Context, args []string) (string }, "helm": func(ctx context.Context, args []string) (string, error) { hc := interp.HandlerCtx(ctx) - path, err := downloader.NewDownloader(commands.NewHelmV3Command(), log.GetFileLogger("shell"), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) + path, err := downloader.NewDownloader(downloaderpkg.NewHelmCommand(), log.GetFileLogger("shell"), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { _, _ = fmt.Fprintln(hc.Stderr, err) return "", interp.NewExitStatus(127) diff --git a/pkg/devspace/pullsecrets/util.go b/pkg/devspace/pullsecrets/util.go index 8fe4fd973a..21bef22ef2 100644 --- a/pkg/devspace/pullsecrets/util.go +++ b/pkg/devspace/pullsecrets/util.go @@ -2,7 +2,7 @@ package pullsecrets import ( "strings" - + "github.com/distribution/reference" dockerregistry "github.com/docker/docker/registry" ) @@ -13,16 +13,16 @@ func GetRegistryFromImageName(imageName string) (string, error) { if err != nil { return "", err } - + repoInfo, err := dockerregistry.ParseRepositoryInfo(ref) if err != nil { return "", err } - + if repoInfo.Index.Official { return "", nil } - + return repoInfo.Index.Name, nil } diff --git a/pkg/util/dockerfile/get.go b/pkg/util/dockerfile/get.go index 53ed338b49..8bade25994 100644 --- a/pkg/util/dockerfile/get.go +++ b/pkg/util/dockerfile/get.go @@ -15,29 +15,29 @@ var findExposePortsRegEx = regexp.MustCompile(`^EXPOSE\s(.*)$`) // GetStrippedDockerImageName returns a tag stripped image name and checks if it's a valid image name func GetStrippedDockerImageName(imageName string) (string, string, error) { imageName = strings.TrimSpace(imageName) - + // Check if we can parse the name ref, err := reference.ParseNormalizedNamed(imageName) if err != nil { return "", "", err } - + // Check if there was a tag tag := "" if refTagged, ok := ref.(reference.NamedTagged); ok { tag = refTagged.Tag() } - + repoInfo, err := dockerregistry.ParseRepositoryInfo(ref) if err != nil { return "", "", err } - + if repoInfo.Index.Official { // strip docker.io and library from image return strings.TrimPrefix(strings.TrimPrefix(reference.TrimNamed(ref).Name(), repoInfo.Index.Name+"/library/"), repoInfo.Index.Name+"/"), tag, nil } - + return reference.TrimNamed(ref).Name(), tag, nil } @@ -47,41 +47,41 @@ func GetPorts(filename string) ([]int, error) { if err != nil { return nil, err } - + data = NormalizeNewlines(data) lines := strings.Split(string(data), "\n") ports := []int{} - + for _, line := range lines { match := findExposePortsRegEx.FindStringSubmatch(line) if match == nil || len(match) != 2 { continue } - + portStrings := strings.Split(match[1], " ") - + OUTER: for _, port := range portStrings { if port == "" { continue } - + intPort, err := strconv.Atoi(strings.Split(port, "/")[0]) if err != nil { return nil, err } - + // Check if port already exists for _, existingPort := range ports { if existingPort == intPort { continue OUTER } } - + ports = append(ports, intPort) } } - + return ports, nil }