From b9f9a4230862bf2e3f2b5945afb6b6d54f739553 Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 15:31:27 -0700 Subject: [PATCH 1/3] Update loft utils for helm v4 Signed-off-by: Ryan Swanson --- cmd/init.go | 248 +++++++++--------- cmd/run.go | 86 +++--- go.mod | 9 +- go.sum | 25 +- .../loader/variable/predefined_variable.go | 19 +- .../deploy/deployer/kubectl/kubectl.go | 105 ++++---- pkg/devspace/helm/client.go | 4 +- pkg/devspace/helm/generic/generic.go | 6 +- pkg/devspace/helm/{v3 => v4}/client.go | 62 ++--- .../pipeline/engine/basichandler/handler.go | 22 +- pkg/util/log/file_logger.go | 12 + pkg/util/log/global.go | 3 +- pkg/util/log/log.go | 7 +- pkg/util/log/logger.go | 25 +- pkg/util/log/logr_adapter.go | 60 +++++ pkg/util/log/stream_logger.go | 23 ++ pkg/util/log/testing/fake.go | 7 +- vendor/github.com/loft-sh/loft-util/LICENSE | 201 -------------- .../loft-sh/loft-util/pkg/command/command.go | 160 ----------- .../loft-sh/loft-util/pkg/command/fake.go | 28 -- .../loft-util/pkg/command/prefixed_saver.go | 83 ------ .../loft-sh/utils/pkg/command/command.go | 9 +- .../utils/pkg/downloader/commands/helm.go | 91 +++++++ .../utils/pkg/downloader/commands/helm_v3.go | 94 +------ .../utils/pkg/downloader/commands/helm_v4.go | 8 + .../utils/pkg/downloader/commands/kubectl.go | 4 +- .../utils/pkg/downloader/downloader.go | 19 +- .../loft-sh/utils/pkg/extract/unzip.go | 3 +- .../loft-sh/utils/pkg/log/logger.go | 32 --- vendor/github.com/otiai10/copy/.gitignore | 4 + vendor/github.com/otiai10/copy/README.md | 36 ++- vendor/github.com/otiai10/copy/copy.go | 149 ++++++----- .../otiai10/copy/copy_namedpipes.go | 3 +- .../otiai10/copy/copy_namedpipes_x.go | 3 +- .../copy/{fileinfo.go => fileinfo_go1.15.go} | 9 +- .../otiai10/copy/fileinfo_go1.16.go | 17 ++ vendor/github.com/otiai10/copy/options.go | 69 ++++- .../otiai10/copy/permission_control.go | 48 ++++ .../otiai10/copy/preserve_ltimes.go | 20 ++ .../otiai10/copy/preserve_ltimes_x.go | 8 + .../github.com/otiai10/copy/preserve_owner.go | 3 +- ...e_owner_windows.go => preserve_owner_x.go} | 3 +- vendor/github.com/otiai10/copy/stat_times.go | 3 +- .../otiai10/copy/stat_times_darwin.go | 1 + .../otiai10/copy/stat_times_freebsd.go | 1 + .../github.com/otiai10/copy/stat_times_js.go | 20 ++ .../otiai10/copy/stat_times_windows.go | 1 + .../github.com/otiai10/copy/stat_times_x.go | 1 + vendor/github.com/otiai10/copy/test_setup.go | 10 +- .../github.com/otiai10/copy/test_setup_x.go | 3 +- vendor/modules.txt | 16 +- vendor/mvdan.cc/sh/v3/expand/arith.go | 61 +++-- vendor/mvdan.cc/sh/v3/expand/expand.go | 124 +++++++-- vendor/mvdan.cc/sh/v3/expand/param.go | 8 +- vendor/mvdan.cc/sh/v3/fileutil/file.go | 18 +- vendor/mvdan.cc/sh/v3/interp/api.go | 150 +++++++++-- vendor/mvdan.cc/sh/v3/interp/builtin.go | 137 +++++++++- vendor/mvdan.cc/sh/v3/interp/handler.go | 4 + vendor/mvdan.cc/sh/v3/interp/runner.go | 21 +- vendor/mvdan.cc/sh/v3/interp/test.go | 2 +- vendor/mvdan.cc/sh/v3/interp/trace.go | 6 +- vendor/mvdan.cc/sh/v3/pattern/pattern.go | 26 +- vendor/mvdan.cc/sh/v3/syntax/lexer.go | 41 +-- vendor/mvdan.cc/sh/v3/syntax/nodes.go | 5 +- vendor/mvdan.cc/sh/v3/syntax/parser.go | 200 +++++++------- vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go | 4 +- vendor/mvdan.cc/sh/v3/syntax/printer.go | 92 ++++--- vendor/mvdan.cc/sh/v3/syntax/simplify.go | 17 +- 68 files changed, 1518 insertions(+), 1281 deletions(-) rename pkg/devspace/helm/{v3 => v4}/client.go (96%) create mode 100644 pkg/util/log/logr_adapter.go delete mode 100644 vendor/github.com/loft-sh/loft-util/LICENSE delete mode 100644 vendor/github.com/loft-sh/loft-util/pkg/command/command.go delete mode 100644 vendor/github.com/loft-sh/loft-util/pkg/command/fake.go delete mode 100644 vendor/github.com/loft-sh/loft-util/pkg/command/prefixed_saver.go create mode 100644 vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go create mode 100644 vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go delete mode 100644 vendor/github.com/loft-sh/utils/pkg/log/logger.go rename vendor/github.com/otiai10/copy/{fileinfo.go => fileinfo_go1.15.go} (74%) create mode 100644 vendor/github.com/otiai10/copy/fileinfo_go1.16.go create mode 100644 vendor/github.com/otiai10/copy/permission_control.go create mode 100644 vendor/github.com/otiai10/copy/preserve_ltimes.go create mode 100644 vendor/github.com/otiai10/copy/preserve_ltimes_x.go rename vendor/github.com/otiai10/copy/{preserve_owner_windows.go => preserve_owner_x.go} (64%) create mode 100644 vendor/github.com/otiai10/copy/stat_times_js.go diff --git a/cmd/init.go b/cmd/init.go index aea903b88c..60abaa0107 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -9,22 +9,22 @@ import ( "regexp" "strconv" "strings" - + "github.com/loft-sh/devspace/pkg/util/ptr" "mvdan.cc/sh/v3/expand" - + "github.com/loft-sh/devspace/pkg/devspace/compose" "github.com/loft-sh/devspace/pkg/devspace/config/localcache" "github.com/sirupsen/logrus" - + "github.com/loft-sh/devspace/cmd/flags" "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" yaml "gopkg.in/yaml.v3" - + "github.com/loft-sh/devspace/pkg/devspace/hook" - + "github.com/loft-sh/devspace/pkg/devspace/plugin" - + "github.com/loft-sh/devspace/pkg/devspace/build/builder/helper" "github.com/loft-sh/devspace/pkg/devspace/config/constants" "github.com/loft-sh/devspace/pkg/devspace/config/loader" @@ -35,7 +35,7 @@ import ( "github.com/loft-sh/devspace/pkg/util/fsutil" "github.com/loft-sh/devspace/pkg/util/log" "github.com/loft-sh/devspace/pkg/util/survey" - "github.com/loft-sh/loft-util/pkg/command" + "github.com/loft-sh/utils/pkg/command" "github.com/mgutz/ansi" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -61,7 +61,7 @@ const ( // InitCmd is a struct that defines a command call for "init" type InitCmd struct { *flags.GlobalFlags - + // Flags Reconfigure bool Dockerfile string @@ -76,7 +76,7 @@ func NewInitCmd(f factory.Factory) *cobra.Command { log: f.GetLog(), GlobalFlags: globalFlags, } - + initCmd := &cobra.Command{ Use: "init", Short: "Initializes DevSpace in the current folder", @@ -94,12 +94,12 @@ folder. Creates a devspace.yaml as a starting point. return cmd.Run(f) }, } - + initCmd.Flags().BoolVarP(&cmd.Reconfigure, "reconfigure", "r", false, "Change existing configuration") initCmd.Flags().StringVar(&cmd.Context, "context", "", "Context path to use for intialization") initCmd.Flags().StringVar(&cmd.Dockerfile, "dockerfile", helper.DefaultDockerfilePath, "Dockerfile to use for initialization") initCmd.Flags().StringVar(&cmd.Provider, "provider", "", "The cloud provider to use") - + return initCmd } @@ -123,39 +123,39 @@ func (cmd *InitCmd) Run(f factory.Factory) error { if err != nil { return err } - + if response == optionNo { return nil } } - + // Delete config & overwrite config os.RemoveAll(".devspace") - + // Delete configs path os.Remove(constants.DefaultConfigsPath) - + // Delete config & overwrite config os.Remove(constants.DefaultConfigPath) - + // Delete config & overwrite config os.Remove(constants.DefaultVarsPath) - + // Execute plugin hook err = hook.ExecuteHooks(nil, nil, "init") if err != nil { return err } - + // Print DevSpace logo log.PrintLogo() - + // Determine if we're initializing from scratch, or using docker-compose.yaml dockerComposePath, generateFromDockerCompose, err := cmd.shouldGenerateFromDockerCompose() if err != nil { return err } - + if generateFromDockerCompose { err = cmd.initDockerCompose(f, dockerComposePath) } else { @@ -164,12 +164,12 @@ func (cmd *InitCmd) Run(f factory.Factory) error { if err != nil { return err } - + cmd.log.WriteString(logrus.InfoLevel, "\n") cmd.log.Done("Project successfully initialized") cmd.log.Info("Configuration saved in devspace.yaml - you can make adjustments as needed") cmd.log.Infof("\r \nYou can now run:\n1. %s - to pick which Kubernetes namespace to work in\n2. %s - to start developing your project in Kubernetes\n\nRun `%s` or `%s` to see a list of available commands and flags\n", ansi.Color("devspace use namespace", "blue+b"), ansi.Color("devspace dev", "blue+b"), ansi.Color("devspace -h", "blue+b"), ansi.Color("devspace [command] -h", "blue+b")) - + return nil } @@ -179,17 +179,17 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + err = languageHandler.CopyTemplates(".", false) if err != nil { return err } - + startScriptAbsPath, err := filepath.Abs(startScriptName) if err != nil { return err } - + _, err = os.Stat(startScriptAbsPath) if err == nil { // Ensure file is executable @@ -198,9 +198,9 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return err } } - + var config *latest.Config - + // create kubectl client client, err := f.NewKubeClientFromContext(cmd.GlobalFlags.KubeContext, cmd.GlobalFlags.Namespace) if err == nil { @@ -209,12 +209,12 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo config = configInterface.Config() } } - + localCache, err := localcache.NewCacheLoader().Load(constants.DefaultConfigPath) if err != nil { return err } - + if config == nil { // Create config config = latest.New().(*latest.Config) @@ -222,22 +222,22 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return err } } - + // Create ConfigureManager configureManager := f.NewConfigureManager(config, localCache, cmd.log) - + // Determine name for this devspace project projectName, projectNamespace, err := getProjectName() if err != nil { return err } - + config.Name = projectName - + imageName := "app" selectedDeploymentOption := "" mustAddComponentChart := false - + for { selectedDeploymentOption, err = cmd.log.Question(&survey.QuestionOptions{ Question: "How do you want to deploy this project?", @@ -250,13 +250,13 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + isQuickstart := strings.HasPrefix(projectName, "devspace-quickstart-") - + if selectedDeploymentOption != DeployOptionHelm && isQuickstart { cmd.log.WriteString(logrus.InfoLevel, "\n") cmd.log.Warn("If this is a DevSpace quickstart project, you should use Helm!") - + useHelm := "Yes" helmAnswer, err := cmd.log.Question(&survey.QuestionOptions{ Question: "Do you want to switch to using Helm as suggested?", @@ -268,12 +268,12 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + if helmAnswer == useHelm { selectedDeploymentOption = DeployOptionHelm } } - + if selectedDeploymentOption == DeployOptionHelm { if isQuickstart { quickstartYes := "Yes" @@ -287,12 +287,12 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + if quickstartAnswer == quickstartYes { mustAddComponentChart = true } } - + if !mustAddComponentChart { hasOwnHelmChart := "Yes" helmChartAnswer, err := cmd.log.Question(&survey.QuestionOptions{ @@ -305,7 +305,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + if helmChartAnswer == hasOwnHelmChart { err = configureManager.AddHelmDeployment(imageName) if err != nil { @@ -313,7 +313,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo cmd.log.WriteString(logrus.InfoLevel, "\n") cmd.log.Errorf("Error: %s", err.Error()) } - + // Retry questions on error continue } @@ -328,14 +328,14 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo cmd.log.WriteString(logrus.InfoLevel, "\n") cmd.log.Errorf("Error: %s", err.Error()) } - + // Retry questions on error continue } } break } - + developProject := "I want to develop this project and my current working dir contains the source code" deployProject := "I just want to deploy this project" defaultProjectAction := deployProject @@ -350,7 +350,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + image := "" if developOrDeployProject == developProject { for { @@ -359,16 +359,16 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return errors.Wrap(err, "error rendering deployment") } - + images, err := parseImages(manifests) if err != nil { return errors.Wrap(err, "error parsing images") } - + imageManual := "Manually enter the image I want to work on" imageSkip := "Skip (do not add dev configuration for any images)" imageAnswer := "" - + if len(images) > 0 { imageAnswer, err = cmd.log.Question(&survey.QuestionOptions{ Question: "Which image do you want to develop with DevSpace?", @@ -387,24 +387,24 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return err } } - + if imageAnswer == imageSkip { break } else if imageAnswer == imageManual { imageQuestion := "What is the main container image of this project?" - + if selectedDeploymentOption == DeployOptionHelm { imageQuestion = "What is the main container image of this project which is deployed by this Helm chart? (e.g. ecr.io/project/image)" } - + if selectedDeploymentOption == DeployOptionKubectl { imageQuestion = "What is the main container image of this project which is deployed by these manifests? (e.g. ecr.io/project/image)" } - + if selectedDeploymentOption == DeployOptionKustomize { imageQuestion = "What is the main container image of this project which is deployed by this Kustomization? (e.g. ecr.io/project/image)" } - + image, err = cmd.log.Question(&survey.QuestionOptions{ Question: imageQuestion, ValidationMessage: "Please enter a valid container image from a Kubernetes pod (e.g. myregistry.tld/project/image)", @@ -420,7 +420,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo image = imageAnswer } } - + err = configureManager.AddImage(imageName, image, projectNamespace+"/"+projectName, cmd.Dockerfile) if err != nil { if err.Error() != "" { @@ -431,13 +431,13 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo } } } - + // Determine app port portString := "" - + if len(config.Images) > 0 { image = config.Images[imageName].Image - + // Try to get ports from dockerfile ports, err := dockerfile.GetPorts(config.Images[imageName].Dockerfile) if err == nil { @@ -451,14 +451,14 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo if err != nil { return err } - + if portString == "" { portString = strconv.Itoa(ports[0]) } } } } - + if portString == "" { portString, err = cmd.log.Question(&survey.QuestionOptions{ Question: "Which port is your application listening on? (Enter to skip)", @@ -468,7 +468,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return err } } - + port := 0 if portString != "" { port, err = strconv.Atoi(portString) @@ -476,7 +476,7 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return errors.Wrap(err, "error parsing port") } } - + // Add component deployment if selected if mustAddComponentChart { err = configureManager.AddComponentDeployment(imageName, image, port) @@ -484,26 +484,26 @@ func (cmd *InitCmd) initDevspace(f factory.Factory, configLoader loader.ConfigLo return err } } - + // Add the development configuration err = cmd.addDevConfig(config, imageName, image, port, languageHandler) if err != nil { return err } - + if config.Commands == nil { config.Commands = map[string]*latest.CommandConfig{} - + config.Commands["migrate-db"] = &latest.CommandConfig{ Command: `echo 'This is a cross-platform, shared command that can be used to codify any kind of dev task.' echo 'Anyone using this project can invoke it via "devspace run migrate-db"'`, } } - + if config.Pipelines == nil { config.Pipelines = map[string]*latest.Pipeline{} } - + // Add pipeline: dev config.Pipelines["dev"] = &latest.Pipeline{ Run: `run_dependencies --all # 1. Deploy any projects this project needs (see "dependencies") @@ -511,7 +511,7 @@ ensure_pull_secrets --all # 2. Ensure pull secrets create_deployments --all # 3. Deploy Helm charts and manifests specfied as "deployments" start_dev ` + imageName + ` # 4. Start dev mode "` + imageName + `" (see "dev" section)`, } - + // Add pipeline: dev config.Pipelines["deploy"] = &latest.Pipeline{ Run: `run_dependencies --all # 1. Deploy any projects this project needs (see "dependencies") @@ -519,31 +519,31 @@ ensure_pull_secrets --all # 2. Ensure pull secrets build_images --all -t $(git describe --always) # 3. Build, tag (git commit hash) and push all images (see "images") create_deployments --all # 4. Deploy Helm charts and manifests specfied as "deployments"`, } - + // Save config err = loader.Save(constants.DefaultConfigPath, config) if err != nil { return err } - + // Save generated err = localCache.Save() if err != nil { return errors.Errorf("Error saving generated file: %v", err) } - + // Add .devspace/ to .gitignore err = appendToIgnoreFile(gitIgnoreFile, devspaceFolderGitignore) if err != nil { cmd.log.Warn(err) } - + configPath := loader.ConfigPath("") err = annotateConfig(configPath) if err != nil { return err } - + return nil } @@ -552,20 +552,20 @@ func (cmd *InitCmd) initDockerCompose(f factory.Factory, composePath string) err if err != nil { return err } - + projectName, _, err := getProjectName() if err != nil { return err } - + project.Name = projectName - + // Prompt user for entrypoints for each container with sync folders. for idx, service := range project.Services { localPaths := compose.GetServiceSyncPaths(project, service) noEntryPoint := len(service.Entrypoint) == 0 hasSyncEndpoints := len(localPaths) > 0 - + if noEntryPoint && hasSyncEndpoints { entrypointStr, err := cmd.log.Question(&survey.QuestionOptions{ Question: "How is this container started? (e.g. npm start, gradle run, go run main.go)", @@ -573,50 +573,50 @@ func (cmd *InitCmd) initDockerCompose(f factory.Factory, composePath string) err if err != nil { return err } - + entrypoint := strings.Split(entrypointStr, " ") project.Services[idx].Entrypoint = entrypoint } } - + // Generate DevSpace configuration composeManager := compose.NewComposeManager(project) err = composeManager.Load(cmd.log) if err != nil { return err } - + // Save each configuration file for path, config := range composeManager.Configs() { localCache, err := localcache.NewCacheLoader().Load(path) if err != nil { return err } - + // Save config err = loader.Save(path, config) if err != nil { return err } - + // Save generated err = localCache.Save() if err != nil { return errors.Errorf("Error saving generated file: %v", err) } - + // Add .devspace/ to .gitignore err = appendToIgnoreFile(gitIgnoreFile, devspaceFolderGitignore) if err != nil { cmd.log.Warn(err) } - + err = annotateConfig(path) if err != nil { return err } } - + return nil } @@ -625,11 +625,11 @@ func annotateConfig(configPath string) error { if err != nil { panic(err) } - + annotatedConfig = regexp.MustCompile("(?m)(\n\\s{2,6}name:.*)").ReplaceAll(annotatedConfig, []byte("")) annotatedConfig = regexp.MustCompile("(?s)(\n deploy:.*)(\n dev:.*)(\nimages:)").ReplaceAll(annotatedConfig, []byte("$2$1$3")) annotatedConfig = regexp.MustCompile("(?s)(\n imageSelector:.*?)(\n.*)(\n devImage:.*?)(\n)").ReplaceAll(annotatedConfig, []byte("$1$3$2$4")) - + configAnnotations := map[string]string{ "(?m)^(pipelines:)": "\n# This is a list of `pipelines` that DevSpace can execute (you can define your own)\n$1", "(?m)^( )(deploy:)": "$1# You can run this pipeline via `devspace deploy` (or `devspace run-pipeline deploy`)\n$1$2", @@ -651,11 +651,11 @@ func annotateConfig(configPath string) error { "(?m)^( )(proxyCommands:)": "$1# Make the following commands from my local machine available inside the dev container\n$1$2", "(?m)^(commands:)": "\n# Use the `commands` section to define repeatable dev workflows for this project \n$1", } - + for expr, replacement := range configAnnotations { annotatedConfig = regexp.MustCompile(expr).ReplaceAll(annotatedConfig, []byte(replacement)) } - + annotatedConfig = append(annotatedConfig, []byte(` # Define dependencies to other projects with a devspace.yaml # dependencies: @@ -665,12 +665,12 @@ func annotateConfig(configPath string) error { # ui: # path: ./ui # Path-based dependencies (for monorepos) `)...) - + err = os.WriteFile(configPath, annotatedConfig, os.ModePerm) if err != nil { return err } - + return nil } @@ -678,21 +678,21 @@ func (cmd *InitCmd) addDevConfig(config *latest.Config, imageName, image string, if config.Dev == nil { config.Dev = map[string]*latest.DevPod{} } - + devConfig, ok := config.Dev[imageName] if !ok { devConfig = &latest.DevPod{} config.Dev[imageName] = devConfig } - + devConfig.ImageSelector = image - + if port > 0 { localPort := port if localPort < 1024 { cmd.log.WriteString(logrus.InfoLevel, "\n") cmd.log.Warn("Your application listens on a system port [0-1024]. Choose a forwarding-port to access your application via localhost.") - + portString, err := cmd.log.Question(&survey.QuestionOptions{ Question: "Which forwarding port [1024-49151] do you want to use to access your application?", DefaultValue: strconv.Itoa(localPort + 8000), @@ -700,13 +700,13 @@ func (cmd *InitCmd) addDevConfig(config *latest.Config, imageName, image string, if err != nil { return err } - + localPort, err = strconv.Atoi(portString) if err != nil { return errors.Errorf("Error parsing port '%s'", portString) } } - + // Add dev.ports portMapping := latest.PortMapping{ Port: fmt.Sprintf("%d", port), @@ -716,12 +716,12 @@ func (cmd *InitCmd) addDevConfig(config *latest.Config, imageName, image string, Port: fmt.Sprintf("%d:%d", localPort, port), } } - + if devConfig.Ports == nil { devConfig.Ports = []*latest.PortMapping{} } devConfig.Ports = append(devConfig.Ports, &portMapping) - + if devConfig.Open == nil { devConfig.Open = []*latest.OpenConfig{} } @@ -729,44 +729,44 @@ func (cmd *InitCmd) addDevConfig(config *latest.Config, imageName, image string, URL: "http://localhost:" + strconv.Itoa(localPort), }) } - + if devConfig.Sync == nil { devConfig.Sync = []*latest.SyncConfig{} } - + syncConfig := &latest.SyncConfig{ Path: "./", } - + if _, err := os.Stat("node_modules"); err == nil { syncConfig.UploadExcludePaths = append(syncConfig.UploadExcludePaths, "node_modules") } - + if _, err := os.Stat(".dockerignore"); err == nil { syncConfig.UploadExcludeFile = ".dockerignore" } - + devConfig.Sync = append(devConfig.Sync, syncConfig) - + devConfig.Terminal = &latest.Terminal{ Command: "./" + startScriptName, } - + devImage, err := languageHandler.GetDevImage() if err != nil { return err } - + devConfig.DevImage = devImage - + devConfig.SSH = &latest.SSH{ Enabled: ptr.Bool(true), } - + if devConfig.ProxyCommands == nil { devConfig.ProxyCommands = []*latest.ProxyCommand{} } - + devConfig.ProxyCommands = append(devConfig.ProxyCommands, []*latest.ProxyCommand{ { Command: "devspace", @@ -781,7 +781,7 @@ func (cmd *InitCmd) addDevConfig(config *latest.Config, imageName, image string, GitCredentials: true, }, }...) - + return nil } @@ -793,7 +793,7 @@ func (cmd *InitCmd) render(f factory.Factory, config *latest.Config) (string, er if err != nil { return "", errors.Wrap(err, "temp render.yaml") } - + silent := true if cmd.Debug { silent = false @@ -816,7 +816,7 @@ func (cmd *InitCmd) render(f factory.Factory, config *latest.Config) (string, er if err != nil { return "", errors.Wrap(err, "devspace render") } - + return writer.String(), nil } @@ -834,7 +834,7 @@ func (cmd *InitCmd) shouldGenerateFromDockerCompose() (string, bool, error) { if err != nil { return "", false, err } - + return dockerComposePath, selectedDockerComposeOption == DockerComposeDevSpaceConfigOption, nil } return "", false, nil @@ -850,14 +850,14 @@ func appendToIgnoreFile(ignoreFile, content string) error { if err != nil { return errors.Errorf("Error reading file %s: %v", ignoreFile, err) } - + // append only if not found in file content if !strings.Contains(string(fileContent), content) { file, err := os.OpenFile(ignoreFile, os.O_APPEND|os.O_WRONLY, 0600) if err != nil { return errors.Errorf("Error writing file %s: %v", ignoreFile, err) } - + defer file.Close() if _, err = file.WriteString(content); err != nil { return errors.Errorf("Error writing file %s: %v", ignoreFile, err) @@ -880,7 +880,7 @@ func getProjectName() (string, string, error) { projectName = projectParts[partsLen-1] } } - + if projectName == "" { absPath, err := filepath.Abs(".") if err != nil { @@ -888,22 +888,22 @@ func getProjectName() (string, string, error) { } projectName = filepath.Base(absPath) } - + projectName = strings.ToLower(projectName) projectName = regexp.MustCompile("[^a-zA-Z0-9- ]+").ReplaceAllString(projectName, "") projectName = regexp.MustCompile("[^a-zA-Z0-9-]+").ReplaceAllString(projectName, "-") projectName = strings.Trim(projectName, "-") - + if !SpaceNameValidationRegEx.MatchString(projectName) || len(projectName) > 42 { projectName = "devspace" } - + return projectName, projectNamespace, nil } func parseImages(manifests string) ([]string, error) { images := []string{} - + var doc yaml.Node dec := yaml.NewDecoder(bytes.NewReader([]byte(manifests))) for dec.Decode(&doc) == nil { @@ -911,16 +911,16 @@ func parseImages(manifests string) ([]string, error) { if err != nil { return nil, err } - + matches, err := path.Find(&doc) if err != nil { return nil, err } - + for _, match := range matches { images = append(images, match.Value) } } - + return images, nil } diff --git a/cmd/run.go b/cmd/run.go index 1692bf7c0f..41bcd47080 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -6,11 +6,11 @@ import ( "io" "os" "strings" - + "github.com/loft-sh/devspace/pkg/devspace/kubectl" "github.com/loft-sh/devspace/pkg/devspace/pipeline/env" "mvdan.cc/sh/v3/expand" - + "github.com/loft-sh/devspace/pkg/devspace/config" "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" @@ -20,9 +20,9 @@ import ( "github.com/loft-sh/devspace/pkg/util/exit" "github.com/loft-sh/devspace/pkg/util/interrupt" "github.com/loft-sh/devspace/pkg/util/log" - "github.com/loft-sh/loft-util/pkg/command" + "github.com/loft-sh/utils/pkg/command" "mvdan.cc/sh/v3/interp" - + "github.com/loft-sh/devspace/cmd/flags" "github.com/loft-sh/devspace/pkg/devspace/config/loader" "github.com/loft-sh/devspace/pkg/devspace/dependency" @@ -30,7 +30,7 @@ import ( flagspkg "github.com/loft-sh/devspace/pkg/util/flags" "github.com/loft-sh/devspace/pkg/util/message" "github.com/sirupsen/logrus" - + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -38,7 +38,7 @@ import ( // RunCmd holds the run cmd flags type RunCmd struct { *flags.GlobalFlags - + Dependency string Stdout io.Writer Stderr io.Writer @@ -51,7 +51,7 @@ func NewRunCmd(f factory.Factory, globalFlags *flags.GlobalFlags, rawConfig *Raw Stdout: os.Stdout, Stderr: os.Stderr, } - + runCmd := &cobra.Command{ Use: "run", DisableFlagParsing: true, @@ -75,11 +75,11 @@ devspace --dependency my-dependency run any-command --any-command-flag if err != nil { return err } - + plugin.SetPluginCommand(cobraCmd, args) return cmd.RunRun(f, args) } - + if rawConfig != nil && rawConfig.Config != nil { for _, cmd := range rawConfig.Config.Commands { runCmd.AddCommand(NewSpecificRunCommand(cmd)) @@ -94,20 +94,20 @@ func (cmd *RunCmd) RunRun(f factory.Factory, args []string) error { if len(args) == 0 { return fmt.Errorf("run requires at least one argument") } - + // check if dependency command commandSplitted := strings.Split(args[0], ".") if len(commandSplitted) > 1 { cmd.Dependency = strings.Join(commandSplitted[:len(commandSplitted)-1], ".") args[0] = commandSplitted[len(commandSplitted)-1] } - + // Execute plugin hook err := hook.ExecuteHooks(nil, nil, "run") if err != nil { return err } - + // Set config root configOptions := cmd.ToConfigOptions() configLoader, err := f.NewConfigLoader(cmd.ConfigPath) @@ -120,45 +120,45 @@ func (cmd *RunCmd) RunRun(f factory.Factory, args []string) error { } else if !configExists { return errors.New(message.ConfigNotFound) } - + // load the config ctx, err := cmd.LoadCommandsConfig(f, configLoader, configOptions, f.GetLog()) if err != nil { return err } - + // check if we should execute a dependency command if cmd.Dependency != "" { config, err := configLoader.LoadWithCache(context.Background(), ctx.Config().LocalCache(), nil, configOptions, f.GetLog()) if err != nil { return err } - + ctx = ctx.WithConfig(config) dependencies, err := f.NewDependencyManager(ctx, configOptions).ResolveAll(ctx, dependency.ResolveOptions{}) if err != nil { return err } - + dep := dependency.GetDependencyByPath(dependencies, cmd.Dependency) if dep == nil { return fmt.Errorf("couldn't find dependency %s", cmd.Dependency) } - + ctx = ctx.AsDependency(dep) commandConfig, err := findCommand(ctx.Config(), args[0]) if err != nil { return err } - + return executeCommandWithAfter(ctx.Context(), commandConfig, args[1:], ctx.Config().Variables(), ctx.WorkingDir(), cmd.Stdout, cmd.Stderr, os.Stdin, ctx.Log()) } - + commandConfig, err := findCommand(ctx.Config(), args[0]) if err != nil { return err } - + return executeCommandWithAfter(ctx.Context(), commandConfig, args[1:], ctx.Config().Variables(), ctx.WorkingDir(), cmd.Stdout, cmd.Stderr, os.Stdin, ctx.Log()) } @@ -167,7 +167,7 @@ func findCommand(config config.Config, name string) (*latest.CommandConfig, erro if config.Config().Commands == nil || config.Config().Commands[name] == nil { return nil, errors.Errorf("couldn't find command '%s' in devspace config", name) } - + return config.Config().Commands[name], nil } @@ -194,7 +194,7 @@ func executeCommandWithAfter(ctx context.Context, command *latest.CommandConfig, return errors.Wrap(err, "error executing after command") } } - + return originalErr } @@ -209,28 +209,28 @@ func ParseArgs(cobraCmd *cobra.Command, globalFlags *flags.GlobalFlags, log log. if index == -1 { return nil, fmt.Errorf("error parsing command: couldn't find %s in command: %v", cobraCmd.Use, os.Args) } - + // check if is help command osArgs := os.Args[:index] if len(os.Args) == index+1 && (os.Args[index] == "-h" || os.Args[index] == "--help") { return nil, cobraCmd.Help() } - + // enable flag parsing cobraCmd.DisableFlagParsing = false - + // apply extra flags _, err := flagspkg.ApplyExtraFlags(cobraCmd, osArgs, true) if err != nil { return nil, err } - + if globalFlags.Silent { log.SetLevel(logrus.FatalLevel) } else if globalFlags.Debug { log.SetLevel(logrus.DebugLevel) } - + args := os.Args[index:] return args, nil } @@ -242,14 +242,14 @@ func (cmd *RunCmd) LoadCommandsConfig(f factory.Factory, configLoader loader.Con if err != nil { return nil, err } - + // try to load client client, err := f.NewKubeClientFromContext(cmd.KubeContext, cmd.Namespace) if err != nil { log.Debugf("Unable to create new kubectl client: %v", err) client = nil } - + // verify client connectivity / authn / authz if client != nil { // If the current kube context or namespace is different than old, @@ -260,13 +260,13 @@ func (cmd *RunCmd) LoadCommandsConfig(f factory.Factory, configLoader loader.Con client = nil } } - + // Parse commands commandsInterface, err := configLoader.LoadWithParser(context.Background(), localCache, client, loader.NewCommandsParser(), configOptions, log) if err != nil { return nil, err } - + // create context return devspacecontext.NewContext(context.Background(), commandsInterface.Variables(), log). WithKubeClient(client). @@ -278,7 +278,7 @@ func executeShellCommand(ctx context.Context, shellCommand string, variables map for k, v := range variables { extraEnv[k] = fmt.Sprintf("%v", v) } - + // execute the command in a shell err := engine.ExecuteSimpleShellCommand(ctx, dir, env.NewVariableEnvProvider(expand.ListEnviron(os.Environ()...), extraEnv), stdout, stderr, stdin, shellCommand, args...) if err != nil { @@ -287,10 +287,10 @@ func executeShellCommand(ctx context.Context, shellCommand string, variables map ExitCode: int(status), } } - + return errors.Wrap(err, "execute command") } - + return nil } @@ -299,7 +299,7 @@ func ExecuteCommand(ctx context.Context, cmd *latest.CommandConfig, variables ma shellCommand := strings.TrimSpace(cmd.Command) shellArgs := cmd.Args appendArgs := cmd.AppendArgs - + extraEnv := map[string]string{} for k, v := range variables { extraEnv[k] = fmt.Sprintf("%v", v) @@ -309,11 +309,11 @@ func ExecuteCommand(ctx context.Context, cmd *latest.CommandConfig, variables ma // Append args to shell command for _, arg := range args { arg = strings.ReplaceAll(arg, "'", "'\"'\"'") - + shellCommand += " '" + arg + "'" } } - + // execute the command in a shell err := engine.ExecuteSimpleShellCommand(ctx, dir, env.NewVariableEnvProvider(expand.ListEnviron(os.Environ()...), extraEnv), stdout, stderr, stdin, shellCommand, args...) if err != nil { @@ -322,13 +322,13 @@ func ExecuteCommand(ctx context.Context, cmd *latest.CommandConfig, variables ma ExitCode: int(status), } } - + return errors.Wrap(err, "execute command") } - + return nil } - + shellArgs = append(shellArgs, args...) return command.Command(ctx, dir, env.NewVariableEnvProvider(expand.ListEnviron(os.Environ()...), extraEnv), stdout, stderr, stdin, shellCommand, shellArgs...) } @@ -336,10 +336,10 @@ func ExecuteCommand(ctx context.Context, cmd *latest.CommandConfig, variables ma // RunCommandCmd holds the cmd flags of a run command type RunCommandCmd struct { *flags.GlobalFlags - + Command *latest.CommandConfig Variables map[string]interface{} - + Stdout io.Writer Stderr io.Writer } @@ -357,7 +357,7 @@ func NewSpecificRunCommand(command *latest.CommandConfig) *cobra.Command { description = description[:61] + "..." } } - + runCmd := &cobra.Command{ Use: command.Name, Short: description, diff --git a/go.mod b/go.mod index 0d224fee05..37cf5bb853 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/fujiwara/shapeio v1.0.0 github.com/gertd/go-pluralize v0.2.0 github.com/gliderlabs/ssh v0.3.5 + github.com/go-logr/logr v1.4.3 github.com/go-resty/resty/v2 v2.7.0 github.com/google/go-containerregistry v0.20.6 github.com/google/uuid v1.6.0 @@ -31,10 +32,9 @@ require ( github.com/json-iterator/go v1.1.12 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/loft-sh/go-github-selfupdate v1.0.0 - github.com/loft-sh/loft-util v0.0.9-alpha github.com/loft-sh/notify v0.0.0-20210827094439-0720dcc7feee github.com/loft-sh/programming-language-detection v0.0.5 - github.com/loft-sh/utils v0.0.16 + github.com/loft-sh/utils v0.0.30 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/mitchellh/go-homedir v1.1.0 github.com/moby/buildkit v0.11.4 @@ -43,7 +43,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.29.0 - github.com/otiai10/copy v1.7.0 + github.com/otiai10/copy v1.11.0 github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.13.1 github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 @@ -67,7 +67,7 @@ require ( k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.110.1 k8s.io/kubectl v0.29.0 - mvdan.cc/sh/v3 v3.5.1 + mvdan.cc/sh/v3 v3.6.0 sigs.k8s.io/yaml v1.4.0 ) @@ -101,7 +101,6 @@ require ( github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect diff --git a/go.sum b/go.sum index 5930af4bf0..7e03becc09 100644 --- a/go.sum +++ b/go.sum @@ -161,8 +161,8 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M= github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA= @@ -335,14 +335,12 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/loft-sh/go-github-selfupdate v1.0.0 h1:YS8iSsIWXw3BygBdPK2xDO4K84XYu2YuYgVS7eQNtik= github.com/loft-sh/go-github-selfupdate v1.0.0/go.mod h1:LDkR6J2QpqQLIMcYvNaSinVwvjPAkg8278oZBPGnrb8= -github.com/loft-sh/loft-util v0.0.9-alpha h1:kGcyTQWxWHWy7bbjhS8Hsq/JRdlSztAU++anV6P+sqk= -github.com/loft-sh/loft-util v0.0.9-alpha/go.mod h1:lsjG5Exh5iEf7Z/87nqwkxx3GRQTizFRLGuS1knF6Cg= github.com/loft-sh/notify v0.0.0-20210827094439-0720dcc7feee h1:hZ79+pKEbCBrH1dVmgZ4jtFrrDPxgM4zqEP1lHlSnvI= github.com/loft-sh/notify v0.0.0-20210827094439-0720dcc7feee/go.mod h1:pq83B8lgfCY7tKdegTTXU6DZxGQkcWMowUTOTpTQmqk= github.com/loft-sh/programming-language-detection v0.0.5 h1:XiWlxtrf4t6Z7SQiob0JMKaCeMHCP3kWhB80wLt+EMY= github.com/loft-sh/programming-language-detection v0.0.5/go.mod h1:QGPQGKr9q1+rQS4OyisS5CPGY1a76SdNaZuk9oy+2cE= -github.com/loft-sh/utils v0.0.16 h1:XnD6Sb6gRWIHgM34U94dHcQ5MtxN5kAGZQ5eddAxC+c= -github.com/loft-sh/utils v0.0.16/go.mod h1:n2L3X4i7d8kb2NF+q5duKa41N+N6fBde6XY2AolgSBI= +github.com/loft-sh/utils v0.0.30 h1:MXtZPeNGjo14PhE1Fu937rUykj3tSUY8Ln2KhdrlOAE= +github.com/loft-sh/utils v0.0.30/go.mod h1:HYzIQwi0vjQt7OmhYDl2z23Md1SNbPJQpw+jZuEM6LQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= @@ -440,13 +438,10 @@ github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/ github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= -github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= -github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= +github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 h1:DiLBVp4DAcZlBVBEtJpNWZpZVq0AEeCY7Hqk8URVs4o= github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= @@ -816,8 +811,8 @@ k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= -mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= +mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU= +mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= diff --git a/pkg/devspace/config/loader/variable/predefined_variable.go b/pkg/devspace/config/loader/variable/predefined_variable.go index ddeccc027d..fca68ec318 100644 --- a/pkg/devspace/config/loader/variable/predefined_variable.go +++ b/pkg/devspace/config/loader/variable/predefined_variable.go @@ -6,26 +6,25 @@ import ( "encoding/json" "errors" "fmt" - "github.com/loft-sh/devspace/pkg/devspace/config/constants" "os" "path/filepath" "strconv" "strings" "time" + "github.com/loft-sh/devspace/pkg/devspace/config/constants" + "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" "github.com/loft-sh/devspace/pkg/devspace/context/values" "github.com/loft-sh/devspace/pkg/devspace/kubectl" - "github.com/loft-sh/devspace/pkg/util/log" - "github.com/loft-sh/utils/pkg/downloader" - "github.com/loft-sh/utils/pkg/downloader/commands" - "github.com/sirupsen/logrus" - - "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" "github.com/loft-sh/devspace/pkg/devspace/plugin" "github.com/loft-sh/devspace/pkg/devspace/upgrade" "github.com/loft-sh/devspace/pkg/util/git" + "github.com/loft-sh/devspace/pkg/util/log" "github.com/loft-sh/devspace/pkg/util/randutil" + "github.com/loft-sh/utils/pkg/downloader" + "github.com/loft-sh/utils/pkg/downloader/commands" "github.com/mitchellh/go-homedir" + "github.com/sirupsen/logrus" ) // PredefinedVariableOptions holds the options for a predefined variable to load @@ -54,9 +53,9 @@ var predefinedVars = map[string]PredefinedVariableFunction{ } return ex, nil }, - "DEVSPACE_KUBECTL_EXECUTABLE": func(ctx context.Context, options *PredefinedVariableOptions, log log.Logger) (interface{}, error) { - debugLog := log.WithLevel(logrus.DebugLevel) - path, err := downloader.NewDownloader(commands.NewKubectlCommand(), debugLog, constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) + "DEVSPACE_KUBECTL_EXECUTABLE": func(ctx context.Context, options *PredefinedVariableOptions, logger log.Logger) (interface{}, error) { + debugLog := logger.WithLevel(logrus.DebugLevel) + path, err := downloader.NewDownloader(commands.NewKubectlCommand(), log.ToLogr(debugLog), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { debugLog.Debugf("Error downloading kubectl: %v", err) return "", nil diff --git a/pkg/devspace/deploy/deployer/kubectl/kubectl.go b/pkg/devspace/deploy/deployer/kubectl/kubectl.go index 7d09239591..7d148d3d46 100644 --- a/pkg/devspace/deploy/deployer/kubectl/kubectl.go +++ b/pkg/devspace/deploy/deployer/kubectl/kubectl.go @@ -6,7 +6,7 @@ import ( "io" "os" "strings" - + "github.com/loft-sh/devspace/pkg/devspace/config/constants" "github.com/loft-sh/devspace/pkg/devspace/config/loader/patch" "github.com/loft-sh/devspace/pkg/devspace/config/loader/variable/legacy" @@ -17,6 +17,7 @@ import ( "github.com/loft-sh/devspace/pkg/devspace/context/values" "github.com/loft-sh/devspace/pkg/devspace/deploy/deployer" "github.com/loft-sh/devspace/pkg/util/hash" + "github.com/loft-sh/devspace/pkg/util/log" "github.com/loft-sh/devspace/pkg/util/stringutil" "github.com/loft-sh/utils/pkg/command" "github.com/loft-sh/utils/pkg/downloader" @@ -39,7 +40,7 @@ type DeployConfig struct { IsInCluster bool InlineManifest string Manifests []string - + DeploymentConfig *latest.DeploymentConfig } @@ -50,7 +51,7 @@ func New(ctx devspacecontext.Context, deployConfig *latest.DeploymentConfig) (de } else if deployConfig.Kubectl.Manifests == nil && deployConfig.Kubectl.InlineManifest == "" { return nil, errors.New("no manifests defined for kubectl deploy") } - + // make sure kubectl exists var ( err error @@ -59,38 +60,38 @@ func New(ctx devspacecontext.Context, deployConfig *latest.DeploymentConfig) (de if deployConfig.Kubectl.KubectlBinaryPath != "" { cmdPath = deployConfig.Kubectl.KubectlBinaryPath } else { - cmdPath, err = downloader.NewDownloader(commands.NewKubectlCommand(), ctx.Log(), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx.Context()) + cmdPath, err = downloader.NewDownloader(commands.NewKubectlCommand(), log.ToLogr(ctx.Log()), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx.Context()) if err != nil { return nil, err } } - + manifests := []string{} for _, ptrManifest := range deployConfig.Kubectl.Manifests { manifest := strings.ReplaceAll(ptrManifest, "*", "") if deployConfig.Kubectl.Kustomize != nil && *deployConfig.Kubectl.Kustomize { manifest = strings.TrimSuffix(manifest, "kustomization.yaml") } - + manifests = append(manifests, manifest) } - + if ctx.KubeClient() == nil { return &DeployConfig{ Name: deployConfig.Name, CmdPath: cmdPath, InlineManifest: deployConfig.Kubectl.InlineManifest, Manifests: manifests, - + DeploymentConfig: deployConfig, }, nil } - + namespace := deployConfig.Namespace if namespace == "" { namespace = ctx.KubeClient().Namespace() } - + return &DeployConfig{ Name: deployConfig.Name, CmdPath: cmdPath, @@ -99,7 +100,7 @@ func New(ctx devspacecontext.Context, deployConfig *latest.DeploymentConfig) (de InlineManifest: deployConfig.Kubectl.InlineManifest, Manifests: manifests, IsInCluster: ctx.KubeClient().IsInCluster(), - + DeploymentConfig: deployConfig, }, nil } @@ -111,11 +112,11 @@ func (d *DeployConfig) Render(ctx devspacecontext.Context, out io.Writer) error if err != nil { return errors.Errorf("%v\nPlease make sure `kubectl apply` does work locally with manifest `%s`", err, manifest) } - + _, _ = out.Write([]byte(replacedManifest)) _, _ = out.Write([]byte("\n---\n")) } - + return nil } @@ -126,7 +127,7 @@ func (d *DeployConfig) Status(ctx devspacecontext.Context) (*deployer.StatusResu if len(manifests) > 20 { manifests = manifests[:20] + "..." } - + return &deployer.StatusResult{ Name: d.Name, Type: "Manifests", @@ -138,7 +139,7 @@ func (d *DeployConfig) Status(ctx devspacecontext.Context) (*deployer.StatusResu // Deploy deploys all specified manifests via kubectl apply and adds to the specified image names the corresponding tags func (d *DeployConfig) Deploy(ctx devspacecontext.Context, _ bool) (bool, error) { deployCache, _ := ctx.Config().RemoteCache().GetDeployment(d.DeploymentConfig.Name) - + // Hash the manifests manifestsHash := "" for _, manifest := range d.Manifests { @@ -146,41 +147,41 @@ func (d *DeployConfig) Deploy(ctx devspacecontext.Context, _ bool) (bool, error) manifestsHash += hash.String(manifest) continue } - + // Check if the chart directory has changed manifest = ctx.ResolvePath(manifest) hash, err := hash.Directory(manifest) if err != nil { return false, errors.Errorf("Error hashing %s: %v", manifest, err) } - + manifestsHash += hash } - + // Hash the deployment config configStr, err := jsonyaml.Marshal(d.DeploymentConfig) if err != nil { return false, errors.Wrap(err, "marshal deployment config") } - + deploymentConfigHash := hash.String(string(configStr)) - + // We force the redeploy of kubectl deployments for now, because we don't know if they are already currently deployed or not, // so it is better to force deploy them, which usually takes almost no time and is better than taking the risk of skipping a needed deployment // forceDeploy = forceDeploy || deployCache.KubectlManifestsHash != manifestsHash || deployCache.DeploymentConfigHash != deploymentConfigHash forceDeploy := true - + ctx.Log().Info("Applying manifests with kubectl...") wasDeployed := false kubeObjects := []remotecache.KubectlObject{} - + for _, manifest := range d.Manifests { wasDeployed, kubeObjects, err = d.applyManifest(ctx, kubeObjects, forceDeploy, false, manifest) if err != nil { return false, err } } - + // Special case for inline manifests if d.InlineManifest != "" { // resolve the runtime variables in the yaml @@ -194,7 +195,7 @@ func (d *DeployConfig) Deploy(ctx devspacecontext.Context, _ bool) (bool, error) return false, err } } - + deployCache.Kubectl = &remotecache.KubectlCache{ Objects: kubeObjects, ManifestsHash: manifestsHash, @@ -214,29 +215,29 @@ func (d *DeployConfig) applyManifest(ctx devspacecontext.Context, kubeObjects [] } writer := ctx.Log().Writer(logrus.InfoLevel, false) defer writer.Close() - + kubeObjects = append(kubeObjects, parsedObjects...) if shouldRedeploy || forceDeploy { args := d.getCmdArgs("apply", "--force") args = append(args, d.DeploymentConfig.Kubectl.ApplyArgs...) - + stdErrBuffer := &bytes.Buffer{} err = command.Command(ctx.Context(), ctx.WorkingDir(), ctx.Environ(), writer, io.MultiWriter(writer, stdErrBuffer), strings.NewReader(replacedManifest), d.CmdPath, args...) if err != nil { return false, nil, errors.Errorf("%v %v\nPlease make sure the command `kubectl apply` does work locally with manifest `%s`", stdErrBuffer.String(), err, manifest) } - + } else { ctx.Log().Infof("Skipping manifest %s", manifest) } - + return true, kubeObjects, nil } func (d *DeployConfig) getReplacedManifest(ctx devspacecontext.Context, inline bool, manifest string) (bool, string, []remotecache.KubectlObject, error) { var objects []*unstructured.Unstructured var err error - + if !inline { objects, err = d.buildManifests(ctx, manifest) if err != nil { @@ -248,30 +249,30 @@ func (d *DeployConfig) getReplacedManifest(ctx devspacecontext.Context, inline b return false, "", nil, err } } - + // Split output into the yamls var ( replaceManifests = []string{} shouldRedeploy = false ) - + kubeObjects := []remotecache.KubectlObject{} for _, resource := range objects { if resource.Object == nil { continue } - + if resource.GetNamespace() == "" { resource.SetNamespace(d.Namespace) } - + kubeObjects = append(kubeObjects, remotecache.KubectlObject{ APIVersion: resource.GetAPIVersion(), Kind: resource.GetKind(), Name: resource.GetName(), Namespace: resource.GetNamespace(), }) - + if d.DeploymentConfig.UpdateImageTags == nil || *d.DeploymentConfig.UpdateImageTags { redeploy, err := legacy.ReplaceImageNamesStringMap(resource.Object, ctx.Config(), ctx.Dependencies(), map[string]bool{"image": true}) if err != nil { @@ -280,21 +281,21 @@ func (d *DeployConfig) getReplacedManifest(ctx devspacecontext.Context, inline b shouldRedeploy = true } } - + resource, err := d.applyDeployPatches(ctx, resource) if err != nil { // we're skipping a patch ctx.Log().Warn(err) } - + replacedManifest, err := jsonyaml.Marshal(resource) if err != nil { return false, "", nil, errors.Wrap(err, "marshal yaml") } - + replaceManifests = append(replaceManifests, string(replacedManifest)) } - + return shouldRedeploy, strings.Join(replaceManifests, "\n---\n"), kubeObjects, nil } @@ -303,12 +304,12 @@ func (d *DeployConfig) getCmdArgs(method string, additionalArgs ...string) []str if d.Context != "" && !d.IsInCluster { args = append(args, "--context", d.Context) } - + args = append(args, method) if additionalArgs != nil { args = append(args, additionalArgs...) } - + args = append(args, "-f", "-") return args } @@ -319,11 +320,11 @@ func (d *DeployConfig) buildManifests(ctx devspacecontext.Context, manifest stri if d.DeploymentConfig.Kubectl.KustomizeBinaryPath != "" { kustomizePath = d.DeploymentConfig.Kubectl.KustomizeBinaryPath } - + if d.DeploymentConfig.Kubectl.Kustomize != nil && *d.DeploymentConfig.Kubectl.Kustomize && d.isKustomizeInstalled(ctx.Context(), ctx.WorkingDir(), kustomizePath) { return NewKustomizeBuilder(kustomizePath, d.DeploymentConfig, ctx.Log()).Build(ctx.Context(), ctx.Environ(), ctx.WorkingDir(), manifest) } - + raw, err := ctx.KubeClient().KubeConfigLoader().LoadRawConfig() if err != nil { return nil, errors.Errorf("get raw config") @@ -332,7 +333,7 @@ func (d *DeployConfig) buildManifests(ctx devspacecontext.Context, manifest stri for key := range copied.Contexts { copied.Contexts[key].Namespace = d.Namespace } - + // Build with kubectl return NewKubectlBuilder(d.CmdPath, d.DeploymentConfig, *copied).Build(ctx.Context(), ctx.Environ(), ctx.WorkingDir(), manifest) } @@ -347,30 +348,30 @@ func (d *DeployConfig) applyDeployPatches(ctx devspacecontext.Context, resource if err != nil { return resource, err } - + patches := patch.Patch{} for idx, kubepatch := range d.DeploymentConfig.Kubectl.Patches { newPatch := patch.Operation{ Op: patch.Op(kubepatch.Operation), Path: patch.OpPath(patch.TransformPath(kubepatch.Path)), } - + if kubepatch.Target.Name != resource.GetName() { continue } - + // non-mandatory field, check only if defined if kubepatch.Target.Kind != "" && resource.GetKind() != kubepatch.Target.Kind { ctx.Log().Debugf("skipping patch, resource kind match: %s - %s", kubepatch.Target.Kind, resource.GetKind()) continue } - + // non-mandatory field, check only if defined if kubepatch.Target.APIVersion != "" && resource.GetAPIVersion() != kubepatch.Target.APIVersion { ctx.Log().Debugf("skipping patch, resource api mismatch: %s - %s", kubepatch.Target.APIVersion, resource.GetAPIVersion()) continue } - + if kubepatch.Value != nil { value, err := patch.NewNode(&kubepatch.Value) if err != nil { @@ -378,23 +379,23 @@ func (d *DeployConfig) applyDeployPatches(ctx devspacecontext.Context, resource } newPatch.Value = value } - + // TODO Maybe log here that we're indeed applying a patch? ctx.Log().Debugf("applying patch: %s.%s", kubepatch.Target.Name, kubepatch.Path) patches = append(patches, newPatch) } - + out, err = patches.Apply(out) if err != nil { return resource, errors.Wrap(err, "apply patches") } - + // transform resource back to unstructured var result unstructured.Unstructured err = jsonyaml.Unmarshal(out, &result) if err != nil { return nil, err } - + return &result, nil } diff --git a/pkg/devspace/helm/client.go b/pkg/devspace/helm/client.go index 9f6be99ebe..284fb3dbb6 100644 --- a/pkg/devspace/helm/client.go +++ b/pkg/devspace/helm/client.go @@ -2,11 +2,11 @@ package helm import ( "github.com/loft-sh/devspace/pkg/devspace/helm/types" - v3 "github.com/loft-sh/devspace/pkg/devspace/helm/v3" + v4 "github.com/loft-sh/devspace/pkg/devspace/helm/v4" "github.com/loft-sh/devspace/pkg/util/log" ) // NewClient creates a new helm client based on the config func NewClient(log log.Logger) (types.Client, error) { - return v3.NewClient(log) + return v4.NewClient(log) } diff --git a/pkg/devspace/helm/generic/generic.go b/pkg/devspace/helm/generic/generic.go index 65b6f66cc8..f52fe8b5be 100644 --- a/pkg/devspace/helm/generic/generic.go +++ b/pkg/devspace/helm/generic/generic.go @@ -28,13 +28,13 @@ type Client interface { WriteValues(values map[string]interface{}) (string, error) } -func NewGenericClient(command commands.Command, log log.Logger) Client { +func NewGenericClient(command commands.Command, logger log.Logger) Client { c := &client{ - log: log, + log: logger, extract: extract.NewExtractor(), } - c.downloader = downloader.NewDownloader(command, log, constants.DefaultHomeDevSpaceFolder) + c.downloader = downloader.NewDownloader(command, log.ToLogr(logger), constants.DefaultHomeDevSpaceFolder) return c } diff --git a/pkg/devspace/helm/v3/client.go b/pkg/devspace/helm/v4/client.go similarity index 96% rename from pkg/devspace/helm/v3/client.go rename to pkg/devspace/helm/v4/client.go index 69b62f326f..e0e58bdd95 100644 --- a/pkg/devspace/helm/v3/client.go +++ b/pkg/devspace/helm/v4/client.go @@ -1,4 +1,4 @@ -package v3 +package v4 import ( "net/url" @@ -6,7 +6,7 @@ import ( "path/filepath" "strconv" "strings" - + "github.com/loft-sh/devspace/pkg/devspace/config/versions/latest" devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context" dependencyutil "github.com/loft-sh/devspace/pkg/devspace/dependency/util" @@ -23,10 +23,10 @@ type client struct { genericHelm generic.Client } -// NewClient creates a new helm v3 Client +// NewClient creates a new helm v4 Client func NewClient(log log.Logger) (types.Client, error) { c := &client{} - c.genericHelm = generic.NewGenericClient(commands.NewHelmV3Command(), log) + c.genericHelm = generic.NewGenericClient(commands.NewHelmV4Command(), log) return c, nil } @@ -38,18 +38,18 @@ func (c *client) DownloadChart(ctx devspacecontext.Context, helmConfig *latest.H return filepath.Dir(chartName), nil } -// InstallChart installs the given chart via helm v3 +// InstallChart installs the given chart via helm v4 func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, releaseNamespace string, values map[string]interface{}, helmConfig *latest.HelmConfig) (*types.Release, error) { valuesFile, err := c.genericHelm.WriteValues(values) if err != nil { return nil, err } defer os.Remove(valuesFile) - + if releaseNamespace == "" { releaseNamespace = ctx.KubeClient().Namespace() } - + args := []string{ "upgrade", releaseName, @@ -57,16 +57,16 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r valuesFile, "--install", } - + // Add debug flag if ctx.Log().GetLevel() == logrus.DebugLevel { args = append(args, "--debug") } - + if releaseNamespace != "" { args = append(args, "--namespace", releaseNamespace) } - + // Chart settings chartPath := "" if helmConfig.Chart.Source != nil { @@ -74,7 +74,7 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r if err != nil { return nil, err } - + chartPath = filepath.Dir(dependencyPath) args = append(args, chartPath) } else { @@ -88,7 +88,7 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r if helmConfig.Chart.Version != "" { args = append(args, "--version", helmConfig.Chart.Version) } - + // log into OCI registry if specified if strings.HasPrefix(chartName, "oci://") { if helmConfig.Chart.Username != "" && helmConfig.Chart.Password != "" { @@ -96,7 +96,7 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r if err != nil { return nil, errors.Wrap(err, "chartName malformed for oci registry") } - + _, err = c.genericHelm.Exec(ctx, []string{"registry", "login", chartNameURL.Hostname(), "--username", helmConfig.Chart.Username, "--password", helmConfig.Chart.Password}) if err != nil { return nil, errors.Wrap(err, "login oci registry") @@ -111,7 +111,7 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r } } } - + // Update dependencies if needed if helmConfig.DisableDependencyUpdate == nil || (helmConfig.DisableDependencyUpdate != nil && !*helmConfig.DisableDependencyUpdate) { stat, err := os.Stat(chartPath) @@ -124,7 +124,7 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r } } } - + // Upgrade options args = append(args, helmConfig.UpgradeArgs...) output, err := c.genericHelm.Exec(ctx, args) @@ -136,18 +136,18 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r if err != nil { return nil, err } - + releases, err := c.ListReleases(ctx, releaseNamespace) if err != nil { return nil, err } - + for _, r := range releases { if r.Name == releaseName && r.Namespace == releaseNamespace { return r, nil } } - + return nil, nil } @@ -157,11 +157,11 @@ func (c *client) Template(ctx devspacecontext.Context, releaseName, releaseNames return "", err } defer os.Remove(valuesFile) - + if releaseNamespace == "" { releaseNamespace = ctx.KubeClient().Namespace() } - + args := []string{ "template", releaseName, @@ -171,7 +171,7 @@ func (c *client) Template(ctx devspacecontext.Context, releaseName, releaseNames if releaseNamespace != "" { args = append(args, "--namespace", releaseNamespace) } - + // Chart settings chartPath := "" if helmConfig.Chart.Source != nil { @@ -179,7 +179,7 @@ func (c *client) Template(ctx devspacecontext.Context, releaseName, releaseNames if err != nil { return "", err } - + chartPath = filepath.Dir(dependencyPath) args = append(args, chartPath) } else { @@ -200,7 +200,7 @@ func (c *client) Template(ctx devspacecontext.Context, releaseName, releaseNames args = append(args, "--password", helmConfig.Chart.Password) } } - + // Update dependencies if needed if helmConfig.DisableDependencyUpdate == nil || (helmConfig.DisableDependencyUpdate != nil && !*helmConfig.DisableDependencyUpdate) { stat, err := os.Stat(chartPath) @@ -218,7 +218,7 @@ func (c *client) Template(ctx devspacecontext.Context, releaseName, releaseNames if err != nil { return "", err } - + return string(result), nil } @@ -226,21 +226,21 @@ func (c *client) DeleteRelease(ctx devspacecontext.Context, releaseName string, if releaseNamespace == "" { releaseNamespace = ctx.KubeClient().Namespace() } - + args := []string{ "delete", releaseName, } - + if releaseNamespace != "" { args = append(args, "--namespace", releaseNamespace) } - + _, err := c.genericHelm.Exec(ctx, args) if err != nil { return err } - + return nil } @@ -255,17 +255,17 @@ func (c *client) ListReleases(ctx devspacecontext.Context, namespace string) ([] if namespace != "" { args = append(args, "--namespace", namespace) } - + out, err := c.genericHelm.Exec(ctx, args) if err != nil { return nil, err } - + releases := []*types.Release{} err = yaml.Unmarshal(out, &releases) if err != nil { return nil, err } - + return releases, nil } diff --git a/pkg/devspace/pipeline/engine/basichandler/handler.go b/pkg/devspace/pipeline/engine/basichandler/handler.go index d6437f6de4..f07af23d7b 100644 --- a/pkg/devspace/pipeline/engine/basichandler/handler.go +++ b/pkg/devspace/pipeline/engine/basichandler/handler.go @@ -5,7 +5,7 @@ import ( "fmt" "os" "time" - + "github.com/loft-sh/devspace/pkg/devspace/config/constants" enginecommands "github.com/loft-sh/devspace/pkg/devspace/pipeline/engine/basichandler/commands" "github.com/loft-sh/devspace/pkg/devspace/pipeline/engine/types" @@ -70,7 +70,7 @@ var OverwriteCommands = map[string]func(ctx context.Context, args []string, hand var EnsureCommands = map[string]func(ctx context.Context, args []string) (string, error){ "kubectl": func(ctx context.Context, args []string) (string, error) { hc := interp.HandlerCtx(ctx) - path, err := downloader.NewDownloader(commands.NewKubectlCommand(), log.GetFileLogger("shell"), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) + path, err := downloader.NewDownloader(commands.NewKubectlCommand(), log.ToLogr(log.GetFileLogger("shell")), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { _, _ = fmt.Fprintln(hc.Stderr, err) return "", interp.NewExitStatus(127) @@ -79,7 +79,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(commands.NewHelmV4Command(), log.ToLogr(log.GetFileLogger("shell")), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { _, _ = fmt.Fprintln(hc.Stderr, err) return "", interp.NewExitStatus(127) @@ -100,10 +100,10 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { return interp.NewExitStatus(255) default: } - + if len(args) > 0 { hc := interp.HandlerCtx(ctx) - + // make sure if we reference devspace in a script we // always use the current binary if args[0] == "devspace" { @@ -112,7 +112,7 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { _, _ = fmt.Fprintln(hc.Stderr, err) return interp.NewExitStatus(1) } - + args[0] = bin } else { // handle overwrite commands @@ -120,7 +120,7 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { if ok { return overwriteCommand(ctx, args[1:], e) } - + // handle some special commands that are not found locally _, err := lookPathDir(hc.Dir, hc.Env, args[0]) if err != nil { @@ -128,7 +128,7 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { if ok { return command(ctx, args[1:]) } - + ensureCommand, ok := EnsureCommands[args[0]] if ok { path, err := ensureCommand(ctx, args[1:]) @@ -141,7 +141,7 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { } } } - + return interp.DefaultExecHandler(2*time.Second)(ctx, args) } @@ -149,12 +149,12 @@ func HandleError(ctx context.Context, command string, err error) error { if err == nil { return interp.NewExitStatus(0) } - + _, ok := interp.IsExitStatus(err) if ok { return err } - + hc := interp.HandlerCtx(ctx) _, _ = fmt.Fprintln(hc.Stderr, errors.Wrap(err, command)) return interp.NewExitStatus(1) diff --git a/pkg/util/log/file_logger.go b/pkg/util/log/file_logger.go index e26c879a08..f0d8d13380 100644 --- a/pkg/util/log/file_logger.go +++ b/pkg/util/log/file_logger.go @@ -371,3 +371,15 @@ func (f *fileLogger) WithPrefixColor(prefix, color string) Logger { func (f *fileLogger) ErrorStreamOnly() Logger { return f } + +func (f *fileLogger) Children() []Logger { + return nil +} + +func (f *fileLogger) Fail(args ...interface{}) { + f.Error(args...) +} + +func (f *fileLogger) Failf(format string, args ...interface{}) { + f.Errorf(format, args...) +} diff --git a/pkg/util/log/global.go b/pkg/util/log/global.go index f03d556c72..2e925d7745 100644 --- a/pkg/util/log/global.go +++ b/pkg/util/log/global.go @@ -2,8 +2,9 @@ package log import ( "fmt" - "github.com/loft-sh/devspace/pkg/util/randutil" "sync" + + "github.com/loft-sh/devspace/pkg/util/randutil" ) var ( diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go index 8fcd804936..85e65eae5f 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -1,13 +1,14 @@ package log import ( + "io" + "os" + "runtime" + "github.com/loft-sh/devspace/pkg/util/scanner" "github.com/mgutz/ansi" "github.com/olekukonko/tablewriter" "github.com/sirupsen/logrus" - "io" - "os" - "runtime" ) var baseLog = NewStdoutLogger(os.Stdin, stdout, stderr, logrus.InfoLevel) diff --git a/pkg/util/log/logger.go b/pkg/util/log/logger.go index 1db6aac089..c86c36b859 100644 --- a/pkg/util/log/logger.go +++ b/pkg/util/log/logger.go @@ -4,7 +4,6 @@ import ( "io" "github.com/loft-sh/devspace/pkg/util/survey" - "github.com/loft-sh/utils/pkg/log" "github.com/sirupsen/logrus" ) @@ -22,7 +21,29 @@ const ( // Logger defines the devspace common logging interface type Logger interface { - log.Logger + Debug(args ...interface{}) + Debugf(format string, args ...interface{}) + Info(args ...interface{}) + Infof(format string, args ...interface{}) + Warn(args ...interface{}) + Warnf(format string, args ...interface{}) + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Done(args ...interface{}) + Donef(format string, args ...interface{}) + Fail(args ...interface{}) + Failf(format string, args ...interface{}) + Print(level logrus.Level, args ...interface{}) + Printf(level logrus.Level, format string, args ...interface{}) + StartWait(message string) + StopWait() + SetLevel(level logrus.Level) + GetLevel() logrus.Level + Children() []Logger + Write(message []byte) (int, error) + // WithLevel creates a new logger with the given level WithLevel(level logrus.Level) Logger Question(params *survey.QuestionOptions) (string, error) diff --git a/pkg/util/log/logr_adapter.go b/pkg/util/log/logr_adapter.go new file mode 100644 index 0000000000..973b66e360 --- /dev/null +++ b/pkg/util/log/logr_adapter.go @@ -0,0 +1,60 @@ +package log + +import ( + "github.com/go-logr/logr" +) + +// LogrSink is an adapter that wraps our Logger interface to implement logr.LogSink +type LogrSink struct { + logger Logger + name string +} + +// Init receives optional information about the logr library +func (l *LogrSink) Init(info logr.RuntimeInfo) {} + +// Enabled tests whether this LogSink is enabled at the specified V-level +func (l *LogrSink) Enabled(level int) bool { + return true +} + +// Info logs a non-error message with the given key/value pairs as context +func (l *LogrSink) Info(level int, msg string, keysAndValues ...any) { + if level > 0 { + l.logger.Debugf("%s %v", msg, keysAndValues) + } else { + l.logger.Infof("%s %v", msg, keysAndValues) + } +} + +// Error logs an error, with the given message and key/value pairs as context +func (l *LogrSink) Error(err error, msg string, keysAndValues ...any) { + if err != nil { + l.logger.Errorf("%s: %v %v", msg, err, keysAndValues) + } else { + l.logger.Errorf("%s %v", msg, keysAndValues) + } +} + +// WithValues returns a new LogSink with additional key/value pairs +func (l *LogrSink) WithValues(keysAndValues ...any) logr.LogSink { + // Our logger doesn't support key-value pairs, just return the same sink + return l +} + +// WithName returns a new LogSink with the specified name appended +func (l *LogrSink) WithName(name string) logr.LogSink { + newName := name + if l.name != "" { + newName = l.name + "/" + name + } + return &LogrSink{ + logger: l.logger.WithPrefix("[" + name + "] "), + name: newName, + } +} + +// ToLogr converts a Logger to a logr.Logger +func ToLogr(logger Logger) logr.Logger { + return logr.New(&LogrSink{logger: logger}) +} diff --git a/pkg/util/log/stream_logger.go b/pkg/util/log/stream_logger.go index b3a6281dae..0a84820cb7 100644 --- a/pkg/util/log/stream_logger.go +++ b/pkg/util/log/stream_logger.go @@ -415,6 +415,14 @@ func (s *StreamLogger) Donef(format string, args ...interface{}) { s.writeMessage(doneFn, fmt.Sprintf(format, args...)+"\n") } +func (s *StreamLogger) Fail(args ...interface{}) { + s.Error(args...) +} + +func (s *StreamLogger) Failf(format string, args ...interface{}) { + s.Errorf(format, args...) +} + func (s *StreamLogger) Print(level logrus.Level, args ...interface{}) { switch level { case logrus.InfoLevel: @@ -459,6 +467,14 @@ func (s *StreamLogger) GetLevel() logrus.Level { return s.level } +func (s *StreamLogger) StartWait(message string) { + // TODO: implement spinner/wait indicator +} + +func (s *StreamLogger) StopWait() { + // TODO: implement spinner/wait indicator +} + func (s *StreamLogger) Writer(level logrus.Level, raw bool) io.WriteCloser { s.m.Lock() defer s.m.Unlock() @@ -496,6 +512,13 @@ func (s *StreamLogger) WriteString(level logrus.Level, message string) { _, _ = s.write(level, []byte(message)) } +func (s *StreamLogger) Write(message []byte) (int, error) { + s.m.Lock() + defer s.m.Unlock() + + return s.write(logrus.InfoLevel, message) +} + func (s *StreamLogger) write(level logrus.Level, message []byte) (int, error) { var ( n int diff --git a/pkg/util/log/testing/fake.go b/pkg/util/log/testing/fake.go index c493a426c3..46ab6726de 100644 --- a/pkg/util/log/testing/fake.go +++ b/pkg/util/log/testing/fake.go @@ -2,9 +2,10 @@ package testing import ( "fmt" - "github.com/loft-sh/devspace/pkg/util/log" "io" + "github.com/loft-sh/devspace/pkg/util/log" + "github.com/loft-sh/devspace/pkg/util/survey" fakesurvey "github.com/loft-sh/devspace/pkg/util/survey/testing" "github.com/sirupsen/logrus" @@ -143,3 +144,7 @@ func (d *FakeLogger) WithPrefixColor(prefix, color string) log.Logger { func (d *FakeLogger) ErrorStreamOnly() log.Logger { return d } + +func (d *FakeLogger) Children() []log.Logger { + return nil +} diff --git a/vendor/github.com/loft-sh/loft-util/LICENSE b/vendor/github.com/loft-sh/loft-util/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/vendor/github.com/loft-sh/loft-util/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/vendor/github.com/loft-sh/loft-util/pkg/command/command.go b/vendor/github.com/loft-sh/loft-util/pkg/command/command.go deleted file mode 100644 index 1f03d3d8f7..0000000000 --- a/vendor/github.com/loft-sh/loft-util/pkg/command/command.go +++ /dev/null @@ -1,160 +0,0 @@ -package command - -import ( - "bytes" - "context" - "fmt" - "io" - "mvdan.cc/sh/v3/expand" - "os" - "os/exec" - "runtime" - "strings" - "time" -) - -// streamCommand is the command whose output is streamed to a log -type streamCommand struct { - cmd *exec.Cmd - killTimeout time.Duration -} - -// newStreamCommand creates a new stream command -func newStreamCommand(command string, args []string) *streamCommand { - return &streamCommand{ - cmd: exec.Command(command, args...), - killTimeout: time.Second * 2, - } -} - -func ListVars(environ expand.Environ) map[string]string { - variables := map[string]string{} - environ.Each(func(name string, vr expand.Variable) bool { - if vr.Kind == expand.String && vr.Str != "" { - variables[name] = vr.Str - } - return true - }) - return variables -} - -// RunWithEnv runs a stream command -func (s *streamCommand) RunWithEnv(ctx context.Context, dir string, environ expand.Environ, stdout io.Writer, stderr io.Writer, stdin io.Reader) error { - s.cmd.Dir = dir - env := []string{} - for k, v := range ListVars(environ) { - env = append(env, k+"="+v) - } - - s.cmd.Env = env - if stdout != nil { - s.cmd.Stdout = stdout - } - - var defaultStderr *prefixSuffixSaver - if stderr != nil { - s.cmd.Stderr = stderr - } else { - defaultStderr = &prefixSuffixSaver{N: 32 << 10} - s.cmd.Stderr = defaultStderr - } - - if stdin != nil { - s.cmd.Stdin = stdin - } - - var err error - err = s.cmd.Start() - if err == nil { - if done := ctx.Done(); done != nil { - go func() { - <-done - - if s.killTimeout <= 0 || runtime.GOOS == "windows" { - _ = s.cmd.Process.Signal(os.Kill) - return - } - - // TODO: don't temporarily leak this goroutine - // if the program stops itself with the - // interrupt. - go func() { - time.Sleep(s.killTimeout) - _ = s.cmd.Process.Signal(os.Kill) - }() - _ = s.cmd.Process.Signal(os.Interrupt) - }() - } - - err = s.cmd.Wait() - } - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok && defaultStderr != nil { - exitErr.Stderr = defaultStderr.Bytes() - } - - return err - } - - return nil -} - -// Run runs a stream command -func (s *streamCommand) Run(ctx context.Context, dir string, stdout io.Writer, stderr io.Writer, stdin io.Reader) error { - return s.RunWithEnv(ctx, dir, expand.ListEnviron(os.Environ()...), stdout, stderr, stdin) -} - -func ShouldExecuteOnOS(os string) bool { - // if the operating system is set and the current is not specified - // we skip the hook - if os != "" { - found := false - oss := strings.Split(os, ",") - for _, os := range oss { - if strings.TrimSpace(os) == runtime.GOOS { - found = true - break - } - } - if !found { - return false - } - } - - return true -} - -func Command(ctx context.Context, dir string, environ expand.Environ, stdout io.Writer, stderr io.Writer, stdin io.Reader, cmd string, args ...string) error { - err := newStreamCommand(cmd, args).RunWithEnv(ctx, dir, environ, stdout, stderr, stdin) - if err != nil { - if errr, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("error executing '%s %s': %s", cmd, strings.Join(args, " "), string(errr.Stderr)) - } - - return err - } - - return nil -} - -func CombinedOutput(ctx context.Context, dir string, environ expand.Environ, cmd string, args ...string) ([]byte, error) { - stdout := &bytes.Buffer{} - err := Command(ctx, dir, environ, stdout, stdout, nil, cmd, args...) - return stdout.Bytes(), err -} - -func Output(ctx context.Context, dir string, environ expand.Environ, cmd string, args ...string) ([]byte, error) { - stdout := &bytes.Buffer{} - err := Command(ctx, dir, environ, stdout, nil, nil, cmd, args...) - return stdout.Bytes(), err -} - -func FormatCommandName(cmd string, args []string) string { - commandString := strings.TrimSpace(cmd + " " + strings.Join(args, " ")) - splitted := strings.Split(commandString, "\n") - if len(splitted) > 1 { - return splitted[0] + "..." - } - - return commandString -} diff --git a/vendor/github.com/loft-sh/loft-util/pkg/command/fake.go b/vendor/github.com/loft-sh/loft-util/pkg/command/fake.go deleted file mode 100644 index 8df07ebc23..0000000000 --- a/vendor/github.com/loft-sh/loft-util/pkg/command/fake.go +++ /dev/null @@ -1,28 +0,0 @@ -package command - -import "io" - -// FakeCommand is used for testing -type FakeCommand struct { - OutputBytes []byte -} - -// CombinedOutput runs the command and returns the stdout and stderr -func (f *FakeCommand) CombinedOutput() ([]byte, error) { - return f.OutputBytes, nil -} - -// Output runs the command and returns the stdout -func (f *FakeCommand) Output() ([]byte, error) { - return f.OutputBytes, nil -} - -// RunWithEnv Run implements interface -func (f *FakeCommand) RunWithEnv(stdout io.Writer, stderr io.Writer, stdin io.Reader, dir string, extraEnvVars map[string]string) error { - return nil -} - -// Run implements interface -func (f *FakeCommand) Run(workingDirectory string, stdout io.Writer, stderr io.Writer, stdin io.Reader) error { - return nil -} diff --git a/vendor/github.com/loft-sh/loft-util/pkg/command/prefixed_saver.go b/vendor/github.com/loft-sh/loft-util/pkg/command/prefixed_saver.go deleted file mode 100644 index bfb4ac7a2e..0000000000 --- a/vendor/github.com/loft-sh/loft-util/pkg/command/prefixed_saver.go +++ /dev/null @@ -1,83 +0,0 @@ -package command - -import ( - "bytes" - "strconv" -) - -// prefixSuffixSaver is an io.Writer which retains the first N bytes -// and the last N bytes written to it. The Bytes() methods reconstructs -// it with a pretty error message. -type prefixSuffixSaver struct { - N int // max size of prefix or suffix - prefix []byte - suffix []byte // ring buffer once len(suffix) == N - suffixOff int // offset to write into suffix - skipped int64 - - // TODO(bradfitz): we could keep one large []byte and use part of it for - // the prefix, reserve space for the '... Omitting N bytes ...' message, - // then the ring buffer suffix, and just rearrange the ring buffer - // suffix when Bytes() is called, but it doesn't seem worth it for - // now just for error messages. It's only ~64KB anyway. -} - -func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { - lenp := len(p) - p = w.fill(&w.prefix, p) - - // Only keep the last w.N bytes of suffix data. - if overage := len(p) - w.N; overage > 0 { - p = p[overage:] - w.skipped += int64(overage) - } - p = w.fill(&w.suffix, p) - - // w.suffix is full now if p is non-empty. Overwrite it in a circle. - for len(p) > 0 { // 0, 1, or 2 iterations. - n := copy(w.suffix[w.suffixOff:], p) - p = p[n:] - w.skipped += int64(n) - w.suffixOff += n - if w.suffixOff == w.N { - w.suffixOff = 0 - } - } - return lenp, nil -} - -// fill appends up to len(p) bytes of p to *dst, such that *dst does not -// grow larger than w.N. It returns the un-appended suffix of p. -func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { - if remain := w.N - len(*dst); remain > 0 { - add := minInt(len(p), remain) - *dst = append(*dst, p[:add]...) - p = p[add:] - } - return p -} - -func (w *prefixSuffixSaver) Bytes() []byte { - if w.suffix == nil { - return w.prefix - } - if w.skipped == 0 { - return append(w.prefix, w.suffix...) - } - var buf bytes.Buffer - buf.Grow(len(w.prefix) + len(w.suffix) + 50) - buf.Write(w.prefix) - buf.WriteString("\n... omitting ") - buf.WriteString(strconv.FormatInt(w.skipped, 10)) - buf.WriteString(" bytes ...\n") - buf.Write(w.suffix[w.suffixOff:]) - buf.Write(w.suffix[:w.suffixOff]) - return buf.Bytes() -} - -func minInt(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/vendor/github.com/loft-sh/utils/pkg/command/command.go b/vendor/github.com/loft-sh/utils/pkg/command/command.go index 8ea86b1314..2c120a4630 100644 --- a/vendor/github.com/loft-sh/utils/pkg/command/command.go +++ b/vendor/github.com/loft-sh/utils/pkg/command/command.go @@ -3,6 +3,7 @@ package command import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -90,7 +91,8 @@ func (s *streamCommand) RunWithEnv(ctx context.Context, dir string, environ expa err = s.cmd.Wait() } if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok && defaultStderr != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && defaultStderr != nil { exitErr.Stderr = defaultStderr.Bytes() } @@ -128,8 +130,9 @@ func ShouldExecuteOnOS(os string) bool { func Command(ctx context.Context, dir string, environ expand.Environ, stdout io.Writer, stderr io.Writer, stdin io.Reader, cmd string, args ...string) error { err := newStreamCommand(cmd, args).RunWithEnv(ctx, dir, environ, stdout, stderr, stdin) if err != nil { - if errr, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("error executing '%s %s': %s", cmd, strings.Join(args, " "), string(errr.Stderr)) + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return fmt.Errorf("error executing '%s %s': %s", cmd, strings.Join(args, " "), string(exitErr.Stderr)) } return err diff --git a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go new file mode 100644 index 0000000000..d45d9208ad --- /dev/null +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go @@ -0,0 +1,91 @@ +package commands + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/loft-sh/utils/pkg/command" + "github.com/loft-sh/utils/pkg/extract" + "github.com/mitchellh/go-homedir" + "github.com/otiai10/copy" + "mvdan.cc/sh/v3/expand" +) + +// helmCommand provides a shared implementation for helm v3 and v4. +type helmCommand struct { + version string + versionPrefix string +} + +func (h *helmCommand) Name() string { + return "helm" +} + +func (h *helmCommand) InstallPath(toolHomeFolder string) (string, error) { + home, err := homedir.Dir() + if err != nil { + return "", err + } + + installPath := filepath.Join(home, toolHomeFolder, "bin", h.Name()) + if runtime.GOOS == "windows" { + installPath += ".exe" + } + + return installPath, nil +} + +func (h *helmCommand) DownloadURL() string { + base := "https://get.helm.sh/helm-" + h.version + "-" + runtime.GOOS + "-" + runtime.GOARCH + if runtime.GOOS == "windows" { + return base + ".zip" + } + return base + ".tar.gz" +} + +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 + } + + return strings.Contains(string(out), h.versionPrefix), nil +} + +func (h *helmCommand) Install(toolHomeFolder, archiveFile string) error { + installPath, err := h.InstallPath(toolHomeFolder) + if err != nil { + return err + } + + return installHelmBinary(extract.NewExtractor(), archiveFile, installPath, h.DownloadURL()) +} + +func installHelmBinary(extractor extract.Extract, archiveFile, installPath, installFromURL string) error { + t := filepath.Dir(archiveFile) + + // Extract the binary + if strings.HasSuffix(installFromURL, ".tar.gz") { + err := extractor.UntarGz(archiveFile, t) + if err != nil { + return fmt.Errorf("extract tar.gz: %w", err) + } + } else if strings.HasSuffix(installFromURL, ".zip") { + err := extractor.Unzip(archiveFile, t) + if err != nil { + return fmt.Errorf("extract zip: %w", err) + } + } + + // Copy file to target location + binaryName := "helm" + if runtime.GOOS == "windows" { + binaryName = "helm.exe" + } + + return copy.Copy(filepath.Join(t, runtime.GOOS+"-"+runtime.GOARCH, binaryName), installPath) +} diff --git a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v3.go b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v3.go index 2ce10dc009..c7f800262a 100644 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v3.go +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v3.go @@ -1,96 +1,8 @@ package commands -import ( - "context" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/loft-sh/utils/pkg/command" - "github.com/loft-sh/utils/pkg/extract" - "github.com/mitchellh/go-homedir" - "github.com/otiai10/copy" - "github.com/pkg/errors" - "mvdan.cc/sh/v3/expand" -) - -var ( - helmVersion = "v3.11.1" - helmDownload = "https://get.helm.sh/helm-" + helmVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH -) - func NewHelmV3Command() Command { - return &helmv3{} -} - -type helmv3 struct{} - -func (h *helmv3) Name() string { - return "helm" -} - -func (h *helmv3) InstallPath(toolHomeFolder string) (string, error) { - home, err := homedir.Dir() - if err != nil { - return "", err - } - - installPath := filepath.Join(home, toolHomeFolder, "bin", h.Name()) - if runtime.GOOS == "windows" { - installPath += ".exe" - } - - return installPath, nil -} - -func (h *helmv3) DownloadURL() string { - url := helmDownload + ".tar.gz" - if runtime.GOOS == "windows" { - url = helmDownload + ".zip" - } - - return url -} - -func (h *helmv3) 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 - } - - return strings.Contains(string(out), `:"v3.`), nil -} - -func (h *helmv3) Install(toolHomeFolder, archiveFile string) error { - installPath, err := h.InstallPath(toolHomeFolder) - if err != nil { - return err + return &helmCommand{ + version: "v3.12.3", + versionPrefix: `:"v3.`, } - - return installHelmBinary(extract.NewExtractor(), archiveFile, installPath, h.DownloadURL()) -} - -func installHelmBinary(extract extract.Extract, archiveFile, installPath, installFromURL string) error { - t := filepath.Dir(archiveFile) - - // Extract the binary - if strings.HasSuffix(installFromURL, ".tar.gz") { - err := extract.UntarGz(archiveFile, t) - if err != nil { - return errors.Wrap(err, "extract tar.gz") - } - } else if strings.HasSuffix(installFromURL, ".zip") { - err := extract.Unzip(archiveFile, t) - if err != nil { - return errors.Wrap(err, "extract zip") - } - } - - // Copy file to target location - if runtime.GOOS == "windows" { - return copy.Copy(filepath.Join(t, runtime.GOOS+"-"+runtime.GOARCH, "helm.exe"), installPath) - } - - return copy.Copy(filepath.Join(t, runtime.GOOS+"-"+runtime.GOARCH, "helm"), installPath) } diff --git a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go new file mode 100644 index 0000000000..6c8c249472 --- /dev/null +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go @@ -0,0 +1,8 @@ +package commands + +func NewHelmV4Command() Command { + return &helmCommand{ + version: "v4.0.4", + versionPrefix: `:"v4.`, + } +} diff --git a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/kubectl.go b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/kubectl.go index ecb6fbc039..f1e940e042 100644 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/kubectl.go +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/commands/kubectl.go @@ -2,7 +2,7 @@ package commands import ( "context" - "io/ioutil" + "io" "net/http" "os" "path/filepath" @@ -47,7 +47,7 @@ func (k *kubectlCommand) DownloadURL() string { // try to fetch latest kubectl version if it fails use default version res, err := http.Get("https://storage.googleapis.com/kubernetes-release/release/stable.txt") if err == nil { - content, err := ioutil.ReadAll(res.Body) + content, err := io.ReadAll(res.Body) res.Body.Close() if err == nil { kubectlVersion = string(content) diff --git a/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go b/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go index 8c187bea31..745c347f36 100644 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go @@ -2,15 +2,14 @@ package downloader import ( "context" + "fmt" "io" "net/http" "os" "path/filepath" + "github.com/go-logr/logr" "github.com/loft-sh/utils/pkg/downloader/commands" - "github.com/loft-sh/utils/pkg/log" - - "github.com/pkg/errors" ) type Downloader interface { @@ -20,11 +19,11 @@ type Downloader interface { type downloader struct { httpGet getRequest command commands.Command - log log.Logger + log logr.Logger toolHomeFolder string } -func NewDownloader(command commands.Command, log log.Logger, toolHomeFolder string) Downloader { +func NewDownloader(command commands.Command, log logr.Logger, toolHomeFolder string) Downloader { return &downloader{ httpGet: http.Get, command: command, @@ -65,12 +64,12 @@ func (d *downloader) downloadExecutable(command, installPath, installFromURL str err = d.downloadFile(command, installPath, installFromURL) if err != nil { - return errors.Wrap(err, "download file") + return fmt.Errorf("download file: %w", err) } err = os.Chmod(installPath, 0755) if err != nil { - return errors.Wrap(err, "cannot make file executable") + return fmt.Errorf("cannot make file executable: %w", err) } return nil @@ -79,7 +78,7 @@ func (d *downloader) downloadExecutable(command, installPath, installFromURL str type getRequest func(url string) (*http.Response, error) func (d *downloader) downloadFile(command, installPath, installFromURL string) error { - d.log.Info("Downloading " + command + "...") + d.log.Info("Downloading", "command", command) t, err := os.MkdirTemp("", "") if err != nil { @@ -100,7 +99,7 @@ func (d *downloader) downloadFile(command, installPath, installFromURL string) e resp, err := d.httpGet(installFromURL) if err != nil { - return errors.Wrap(err, "get url") + return fmt.Errorf("get url: %w", err) } defer func(Body io.ReadCloser) { @@ -109,7 +108,7 @@ func (d *downloader) downloadFile(command, installPath, installFromURL string) e _, err = io.Copy(f, resp.Body) if err != nil { - return errors.Wrap(err, "download file") + return fmt.Errorf("download file: %w", err) } err = f.Close() diff --git a/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go b/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go index 2c7dccf057..32985b557e 100644 --- a/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go +++ b/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go @@ -4,6 +4,7 @@ import ( "archive/tar" archivezip "archive/zip" "compress/gzip" + "errors" "fmt" "io" "os" @@ -41,7 +42,7 @@ func (e *extractor) UntarGz(src, dest string) error { tarReader := tar.NewReader(uncompressedStream) for { header, err := tarReader.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } diff --git a/vendor/github.com/loft-sh/utils/pkg/log/logger.go b/vendor/github.com/loft-sh/utils/pkg/log/logger.go deleted file mode 100644 index 39705da4cc..0000000000 --- a/vendor/github.com/loft-sh/utils/pkg/log/logger.go +++ /dev/null @@ -1,32 +0,0 @@ -package log - -import ( - "github.com/sirupsen/logrus" -) - -// Logger defines the common logging interface -type Logger interface { - Debug(args ...interface{}) - Debugf(format string, args ...interface{}) - - Info(args ...interface{}) - Infof(format string, args ...interface{}) - - Done(args ...interface{}) - Donef(format string, args ...interface{}) - - Warn(args ...interface{}) - Warnf(format string, args ...interface{}) - - Error(args ...interface{}) - Errorf(format string, args ...interface{}) - - Fatal(args ...interface{}) - Fatalf(format string, args ...interface{}) - - Print(level logrus.Level, args ...interface{}) - Printf(level logrus.Level, format string, args ...interface{}) - - SetLevel(level logrus.Level) - GetLevel() logrus.Level -} diff --git a/vendor/github.com/otiai10/copy/.gitignore b/vendor/github.com/otiai10/copy/.gitignore index d65ce17257..a793485586 100644 --- a/vendor/github.com/otiai10/copy/.gitignore +++ b/vendor/github.com/otiai10/copy/.gitignore @@ -1,5 +1,9 @@ test/data.copy +test/owned-by-root coverage.txt vendor .vagrant .idea/ + +# Test Specific +test/data/case16/large.file diff --git a/vendor/github.com/otiai10/copy/README.md b/vendor/github.com/otiai10/copy/README.md index 2305fbfd55..dc66c4e95a 100644 --- a/vendor/github.com/otiai10/copy/README.md +++ b/vendor/github.com/otiai10/copy/README.md @@ -16,7 +16,17 @@ # Example Usage ```go -err := Copy("your/directory", "your/directory.copy") +package main + +import ( + "fmt" + cp "github.com/otiai10/copy" +) + +func main() { + err := cp.Copy("your/src", "your/dest") + fmt.Println(err) // nil +} ``` # Advanced Usage @@ -31,12 +41,24 @@ type Options struct { // OnDirExists can specify what to do when there is a directory already existing in destination. OnDirExists func(src, dest string) DirExistsAction - // Skip can specify which files should be skipped - Skip func(src string) (bool, error) + // OnError can let users decide how to handle errors (e.g., you can suppress specific error). + OnError func(src, dest, string, err error) error - // AddPermission to every entry, - // NO MORE THAN 0777 - AddPermission os.FileMode + // Skip can specify which files should be skipped + Skip func(srcinfo os.FileInfo, src, dest string) (bool, error) + + // PermissionControl can control permission of + // every entry. + // When you want to add permission 0222, do like + // + // PermissionControl = AddPermission(0222) + // + // or if you even don't want to touch permission, + // + // PermissionControl = DoNothing + // + // By default, PermissionControl = PreservePermission + PermissionControl PermissionControlFunc // Sync file after copy. // Useful in case when file must be on the disk @@ -61,7 +83,7 @@ type Options struct { ```go // For example... opt := Options{ - Skip: func(src string) (bool, error) { + Skip: func(info os.FileInfo, src, dest string) (bool, error) { return strings.HasSuffix(src, ".git"), nil }, } diff --git a/vendor/github.com/otiai10/copy/copy.go b/vendor/github.com/otiai10/copy/copy.go index ae04036b83..a84ff7bae1 100644 --- a/vendor/github.com/otiai10/copy/copy.go +++ b/vendor/github.com/otiai10/copy/copy.go @@ -8,13 +8,6 @@ import ( "time" ) -const ( - // tmpPermissionForDirectory makes the destination directory writable, - // so that stuff can be copied recursively even if any original directory is NOT writable. - // See https://github.com/otiai10/copy/pull/9 for more information. - tmpPermissionForDirectory = os.FileMode(0755) -) - type timespec struct { Mtime time.Time Atime time.Time @@ -22,17 +15,22 @@ type timespec struct { } // Copy copies src to dest, doesn't matter if src is a directory or a file. -func Copy(src, dest string, opt ...Options) error { +func Copy(src, dest string, opts ...Options) error { + opt := assureOptions(src, dest, opts...) info, err := os.Lstat(src) if err != nil { - return err + return onError(src, dest, err, opt) } - return switchboard(src, dest, info, assure(src, dest, opt...)) + return switchboard(src, dest, info, opt) } // switchboard switches proper copy functions regarding file type, etc... // If there would be anything else here, add a case to this switchboard. func switchboard(src, dest string, info os.FileInfo, opt Options) (err error) { + if info.Mode()&os.ModeDevice != 0 && !opt.Specials { + return onError(src, dest, err, opt) + } + switch { case info.Mode()&os.ModeSymlink != 0: err = onsymlink(src, dest, opt) @@ -44,19 +42,21 @@ func switchboard(src, dest string, info os.FileInfo, opt Options) (err error) { err = fcopy(src, dest, info, opt) } - return err + return onError(src, dest, err, opt) } // copyNextOrSkip decide if this src should be copied or not. // Because this "copy" could be called recursively, // "info" MUST be given here, NOT nil. func copyNextOrSkip(src, dest string, info os.FileInfo, opt Options) error { - skip, err := opt.Skip(src) - if err != nil { - return err - } - if skip { - return nil + if opt.Skip != nil { + skip, err := opt.Skip(info, src, dest) + if err != nil { + return err + } + if skip { + return nil + } } return switchboard(src, dest, info, opt) } @@ -65,6 +65,14 @@ func copyNextOrSkip(src, dest string, info os.FileInfo, opt Options) error { // with considering existence of parent directory // and file permission. func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { + s, err := os.Open(src) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return + } + defer fclose(s, &err) if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { return @@ -76,19 +84,20 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { } defer fclose(f, &err) - if err = os.Chmod(f.Name(), info.Mode()|opt.AddPermission); err != nil { - return - } - - s, err := os.Open(src) + chmodfunc, err := opt.PermissionControl(info, dest) if err != nil { - return + return err } - defer fclose(s, &err) + chmodfunc(&err) var buf []byte = nil var w io.Writer = f - // var r io.Reader = s + var r io.Reader = s + + if opt.WrapReader != nil { + r = opt.WrapReader(s) + } + if opt.CopyBufferSize != 0 { buf = make([]byte, opt.CopyBufferSize) // Disable using `ReadFrom` by io.CopyBuffer. @@ -96,7 +105,8 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { w = struct{ io.Writer }{f} // r = struct{ io.Reader }{s} } - if _, err = io.CopyBuffer(w, s, buf); err != nil { + + if _, err = io.CopyBuffer(w, r, buf); err != nil { return err } @@ -122,32 +132,24 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { // with scanning contents inside the directory // and pass everything to "copy" recursively. func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) { - - _, err = os.Stat(destdir) - if err == nil && opt.OnDirExists != nil && destdir != opt.intent.dest { - switch opt.OnDirExists(srcdir, destdir) { - case Replace: - if err := os.RemoveAll(destdir); err != nil { - return err - } - case Untouchable: - return nil - } // case "Merge" is default behaviour. Go through. - } else if err != nil && !os.IsNotExist(err) { - return err // Unwelcome error type...! + if skip, err := onDirExists(opt, srcdir, destdir); err != nil { + return err + } else if skip { + return nil } - originalMode := info.Mode() - // Make dest dir with 0755 so that everything writable. - if err = os.MkdirAll(destdir, tmpPermissionForDirectory); err != nil { - return + chmodfunc, err := opt.PermissionControl(info, destdir) + if err != nil { + return err } - // Recover dir mode with original one. - defer chmod(destdir, originalMode|opt.AddPermission, &err) + defer chmodfunc(&err) contents, err := ioutil.ReadDir(srcdir) if err != nil { + if os.IsNotExist(err) { + return nil + } return } @@ -175,10 +177,33 @@ func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) { return } +func onDirExists(opt Options, srcdir, destdir string) (bool, error) { + _, err := os.Stat(destdir) + if err == nil && opt.OnDirExists != nil && destdir != opt.intent.dest { + switch opt.OnDirExists(srcdir, destdir) { + case Replace: + if err := os.RemoveAll(destdir); err != nil { + return false, err + } + case Untouchable: + return true, nil + } // case "Merge" is default behaviour. Go through. + } else if err != nil && !os.IsNotExist(err) { + return true, err // Unwelcome error type...! + } + return false, nil +} + func onsymlink(src, dest string, opt Options) error { switch opt.OnSymlink(src) { case Shallow: - return lcopy(src, dest) + if err := lcopy(src, dest); err != nil { + return err + } + if opt.PreserveTimes { + return preserveLtimes(src, dest) + } + return nil case Deep: orig, err := os.Readlink(src) if err != nil { @@ -201,6 +226,9 @@ func onsymlink(src, dest string, opt Options) error { func lcopy(src, dest string) error { src, err := os.Readlink(src) if err != nil { + if os.IsNotExist(err) { + return nil + } return err } return os.Symlink(src, dest) @@ -215,29 +243,12 @@ func fclose(f *os.File, reported *error) { } } -// chmod ANYHOW changes file mode, -// with asiging error raised during Chmod, -// BUT respecting the error already reported. -func chmod(dir string, mode os.FileMode, reported *error) { - if err := os.Chmod(dir, mode); *reported == nil { - *reported = err +// onError lets caller to handle errors +// occured when copying a file. +func onError(src, dest string, err error, opt Options) error { + if opt.OnError == nil { + return err } -} -// assure Options struct, should be called only once. -// All optional values MUST NOT BE nil/zero after assured. -func assure(src, dest string, opts ...Options) Options { - defopt := getDefaultOptions(src, dest) - if len(opts) == 0 { - return defopt - } - if opts[0].OnSymlink == nil { - opts[0].OnSymlink = defopt.OnSymlink - } - if opts[0].Skip == nil { - opts[0].Skip = defopt.Skip - } - opts[0].intent.src = defopt.intent.src - opts[0].intent.dest = defopt.intent.dest - return opts[0] + return opt.OnError(src, dest, err) } diff --git a/vendor/github.com/otiai10/copy/copy_namedpipes.go b/vendor/github.com/otiai10/copy/copy_namedpipes.go index 48784e7640..615ddcd554 100644 --- a/vendor/github.com/otiai10/copy/copy_namedpipes.go +++ b/vendor/github.com/otiai10/copy/copy_namedpipes.go @@ -1,4 +1,5 @@ -// +build !windows,!plan9,!netbsd,!aix,!illumos,!solaris +//go:build !windows && !plan9 && !netbsd && !aix && !illumos && !solaris && !js +// +build !windows,!plan9,!netbsd,!aix,!illumos,!solaris,!js package copy diff --git a/vendor/github.com/otiai10/copy/copy_namedpipes_x.go b/vendor/github.com/otiai10/copy/copy_namedpipes_x.go index 2f6c74740a..38dd9dc724 100644 --- a/vendor/github.com/otiai10/copy/copy_namedpipes_x.go +++ b/vendor/github.com/otiai10/copy/copy_namedpipes_x.go @@ -1,4 +1,5 @@ -// +build windows plan9 netbsd aix illumos solaris +//go:build windows || plan9 || netbsd || aix || illumos || solaris || js +// +build windows plan9 netbsd aix illumos solaris js package copy diff --git a/vendor/github.com/otiai10/copy/fileinfo.go b/vendor/github.com/otiai10/copy/fileinfo_go1.15.go similarity index 74% rename from vendor/github.com/otiai10/copy/fileinfo.go rename to vendor/github.com/otiai10/copy/fileinfo_go1.15.go index 0b32abeb40..c0708eaf11 100644 --- a/vendor/github.com/otiai10/copy/fileinfo.go +++ b/vendor/github.com/otiai10/copy/fileinfo_go1.15.go @@ -1,12 +1,17 @@ +//go:build !go1.16 +// +build !go1.16 + package copy +import "os" + // This is a cloned definition of os.FileInfo (go1.15) or fs.FileInfo (go1.16~) // A FileInfo describes a file and is returned by Stat. type fileInfo interface { // Name() string // base name of the file // Size() int64 // length in bytes for regular files; system-dependent for others - // Mode() FileMode // file mode bits + Mode() os.FileMode // file mode bits // ModTime() time.Time // modification time - // IsDir() bool // abbreviation for Mode().IsDir() + IsDir() bool // abbreviation for Mode().IsDir() Sys() interface{} // underlying data source (can return nil) } diff --git a/vendor/github.com/otiai10/copy/fileinfo_go1.16.go b/vendor/github.com/otiai10/copy/fileinfo_go1.16.go new file mode 100644 index 0000000000..01b3fd2499 --- /dev/null +++ b/vendor/github.com/otiai10/copy/fileinfo_go1.16.go @@ -0,0 +1,17 @@ +//go:build go1.16 +// +build go1.16 + +package copy + +import "io/fs" + +// This is a cloned definition of os.FileInfo (go1.15) or fs.FileInfo (go1.16~) +// A FileInfo describes a file and is returned by Stat. +type fileInfo interface { + // Name() string // base name of the file + // Size() int64 // length in bytes for regular files; system-dependent for others + Mode() fs.FileMode // file mode bits + // ModTime() time.Time // modification time + IsDir() bool // abbreviation for Mode().IsDir() + Sys() interface{} // underlying data source (can return nil) +} diff --git a/vendor/github.com/otiai10/copy/options.go b/vendor/github.com/otiai10/copy/options.go index f598fc10de..52d636c95d 100644 --- a/vendor/github.com/otiai10/copy/options.go +++ b/vendor/github.com/otiai10/copy/options.go @@ -1,6 +1,9 @@ package copy -import "os" +import ( + "io" + "os" +) // Options specifies optional actions on copying. type Options struct { @@ -11,13 +14,29 @@ type Options struct { // OnDirExists can specify what to do when there is a directory already existing in destination. OnDirExists func(src, dest string) DirExistsAction + // OnErr lets called decide whether or not to continue on particular copy error. + OnError func(src, dest string, err error) error + // Skip can specify which files should be skipped - Skip func(src string) (bool, error) + Skip func(srcinfo os.FileInfo, src, dest string) (bool, error) + + // Specials includes special files to be copied. default false. + Specials bool // AddPermission to every entities, // NO MORE THAN 0777 + // @OBSOLETE + // Use `PermissionControl = AddPermission(perm)` instead AddPermission os.FileMode + // PermissionControl can preserve or even add permission to + // every entries, for example + // + // opt.PermissionControl = AddPermission(0222) + // + // See permission_control.go for more detail. + PermissionControl PermissionControlFunc + // Sync file after copy. // Useful in case when file must be on the disk // (in case crash happens, for example), @@ -36,6 +55,11 @@ type Options struct { // See https://golang.org/pkg/io/#CopyBuffer for more information. CopyBufferSize uint + // If you want to add some limitation on reading src file, + // you can wrap the src and provide new reader, + // such as `RateLimitReader` in the test case. + WrapReader func(src *os.File) io.Reader + intent struct { src string dest string @@ -73,17 +97,42 @@ func getDefaultOptions(src, dest string) Options { OnSymlink: func(string) SymlinkAction { return Shallow // Do shallow copy }, - OnDirExists: nil, // Default behavior is "Merge". - Skip: func(string) (bool, error) { - return false, nil // Don't skip - }, - AddPermission: 0, // Add nothing - Sync: false, // Do not sync - PreserveTimes: false, // Do not preserve the modification time - CopyBufferSize: 0, // Do not specify, use default bufsize (32*1024) + OnDirExists: nil, // Default behavior is "Merge". + OnError: nil, // Default is "accept error" + Skip: nil, // Do not skip anything + AddPermission: 0, // Add nothing + PermissionControl: PerservePermission, // Just preserve permission + Sync: false, // Do not sync + Specials: false, // Do not copy special files + PreserveTimes: false, // Do not preserve the modification time + CopyBufferSize: 0, // Do not specify, use default bufsize (32*1024) + WrapReader: nil, // Do not wrap src files, use them as they are. intent: struct { src string dest string }{src, dest}, } } + +// assureOptions struct, should be called only once. +// All optional values MUST NOT BE nil/zero after assured. +func assureOptions(src, dest string, opts ...Options) Options { + defopt := getDefaultOptions(src, dest) + if len(opts) == 0 { + return defopt + } + if opts[0].OnSymlink == nil { + opts[0].OnSymlink = defopt.OnSymlink + } + if opts[0].Skip == nil { + opts[0].Skip = defopt.Skip + } + if opts[0].AddPermission > 0 { + opts[0].PermissionControl = AddPermission(opts[0].AddPermission) + } else if opts[0].PermissionControl == nil { + opts[0].PermissionControl = PerservePermission + } + opts[0].intent.src = defopt.intent.src + opts[0].intent.dest = defopt.intent.dest + return opts[0] +} diff --git a/vendor/github.com/otiai10/copy/permission_control.go b/vendor/github.com/otiai10/copy/permission_control.go new file mode 100644 index 0000000000..97ae12d8e0 --- /dev/null +++ b/vendor/github.com/otiai10/copy/permission_control.go @@ -0,0 +1,48 @@ +package copy + +import ( + "os" +) + +const ( + // tmpPermissionForDirectory makes the destination directory writable, + // so that stuff can be copied recursively even if any original directory is NOT writable. + // See https://github.com/otiai10/copy/pull/9 for more information. + tmpPermissionForDirectory = os.FileMode(0755) +) + +type PermissionControlFunc func(srcinfo fileInfo, dest string) (chmodfunc func(*error), err error) + +var ( + AddPermission = func(perm os.FileMode) PermissionControlFunc { + return func(srcinfo fileInfo, dest string) (func(*error), error) { + orig := srcinfo.Mode() + if srcinfo.IsDir() { + if err := os.MkdirAll(dest, tmpPermissionForDirectory); err != nil { + return func(*error) {}, err + } + } + return func(err *error) { + chmod(dest, orig|perm, err) + }, nil + } + } + PerservePermission PermissionControlFunc = AddPermission(0) + DoNothing PermissionControlFunc = func(srcinfo fileInfo, dest string) (func(*error), error) { + if srcinfo.IsDir() { + if err := os.MkdirAll(dest, srcinfo.Mode()); err != nil { + return func(*error) {}, err + } + } + return func(*error) {}, nil + } +) + +// chmod ANYHOW changes file mode, +// with asiging error raised during Chmod, +// BUT respecting the error already reported. +func chmod(dir string, mode os.FileMode, reported *error) { + if err := os.Chmod(dir, mode); *reported == nil { + *reported = err + } +} diff --git a/vendor/github.com/otiai10/copy/preserve_ltimes.go b/vendor/github.com/otiai10/copy/preserve_ltimes.go new file mode 100644 index 0000000000..cc006d3750 --- /dev/null +++ b/vendor/github.com/otiai10/copy/preserve_ltimes.go @@ -0,0 +1,20 @@ +//go:build !windows && !plan9 && !js +// +build !windows,!plan9,!js + +package copy + +import ( + "golang.org/x/sys/unix" +) + +func preserveLtimes(src, dest string) error { + info := new(unix.Stat_t) + if err := unix.Lstat(src, info); err != nil { + return err + } + + return unix.Lutimes(dest, []unix.Timeval{ + unix.NsecToTimeval(info.Atim.Nano()), + unix.NsecToTimeval(info.Mtim.Nano()), + }) +} diff --git a/vendor/github.com/otiai10/copy/preserve_ltimes_x.go b/vendor/github.com/otiai10/copy/preserve_ltimes_x.go new file mode 100644 index 0000000000..02aec40be6 --- /dev/null +++ b/vendor/github.com/otiai10/copy/preserve_ltimes_x.go @@ -0,0 +1,8 @@ +//go:build windows || js || plan9 +// +build windows js plan9 + +package copy + +func preserveLtimes(src, dest string) error { + return nil // Unsupported +} diff --git a/vendor/github.com/otiai10/copy/preserve_owner.go b/vendor/github.com/otiai10/copy/preserve_owner.go index 0401d9f27c..13ec4f5793 100644 --- a/vendor/github.com/otiai10/copy/preserve_owner.go +++ b/vendor/github.com/otiai10/copy/preserve_owner.go @@ -1,4 +1,5 @@ -//+build !windows +//go:build !windows && !plan9 +// +build !windows,!plan9 package copy diff --git a/vendor/github.com/otiai10/copy/preserve_owner_windows.go b/vendor/github.com/otiai10/copy/preserve_owner_x.go similarity index 64% rename from vendor/github.com/otiai10/copy/preserve_owner_windows.go rename to vendor/github.com/otiai10/copy/preserve_owner_x.go index 4ebb5cf1dd..9d8257400b 100644 --- a/vendor/github.com/otiai10/copy/preserve_owner_windows.go +++ b/vendor/github.com/otiai10/copy/preserve_owner_x.go @@ -1,4 +1,5 @@ -//+build windows +//go:build windows || plan9 +// +build windows plan9 package copy diff --git a/vendor/github.com/otiai10/copy/stat_times.go b/vendor/github.com/otiai10/copy/stat_times.go index e72de944da..75f45f6e29 100644 --- a/vendor/github.com/otiai10/copy/stat_times.go +++ b/vendor/github.com/otiai10/copy/stat_times.go @@ -1,4 +1,5 @@ -// +build !windows,!darwin,!freebsd,!plan9,!netbsd +//go:build !windows && !darwin && !freebsd && !plan9 && !netbsd && !js +// +build !windows,!darwin,!freebsd,!plan9,!netbsd,!js // TODO: add more runtimes diff --git a/vendor/github.com/otiai10/copy/stat_times_darwin.go b/vendor/github.com/otiai10/copy/stat_times_darwin.go index ce7a7fbc7f..d4c23d8ef2 100644 --- a/vendor/github.com/otiai10/copy/stat_times_darwin.go +++ b/vendor/github.com/otiai10/copy/stat_times_darwin.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin package copy diff --git a/vendor/github.com/otiai10/copy/stat_times_freebsd.go b/vendor/github.com/otiai10/copy/stat_times_freebsd.go index 115f1388b4..5309334ef9 100644 --- a/vendor/github.com/otiai10/copy/stat_times_freebsd.go +++ b/vendor/github.com/otiai10/copy/stat_times_freebsd.go @@ -1,3 +1,4 @@ +//go:build freebsd // +build freebsd package copy diff --git a/vendor/github.com/otiai10/copy/stat_times_js.go b/vendor/github.com/otiai10/copy/stat_times_js.go new file mode 100644 index 0000000000..c645771cab --- /dev/null +++ b/vendor/github.com/otiai10/copy/stat_times_js.go @@ -0,0 +1,20 @@ +//go:build js +// +build js + +package copy + +import ( + "os" + "syscall" + "time" +) + +func getTimeSpec(info os.FileInfo) timespec { + stat := info.Sys().(*syscall.Stat_t) + times := timespec{ + Mtime: info.ModTime(), + Atime: time.Unix(int64(stat.Atime), int64(stat.AtimeNsec)), + Ctime: time.Unix(int64(stat.Ctime), int64(stat.CtimeNsec)), + } + return times +} diff --git a/vendor/github.com/otiai10/copy/stat_times_windows.go b/vendor/github.com/otiai10/copy/stat_times_windows.go index 113a2ece58..d6a84a7693 100644 --- a/vendor/github.com/otiai10/copy/stat_times_windows.go +++ b/vendor/github.com/otiai10/copy/stat_times_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package copy diff --git a/vendor/github.com/otiai10/copy/stat_times_x.go b/vendor/github.com/otiai10/copy/stat_times_x.go index 7b2d1d44f6..886ddd3fd0 100644 --- a/vendor/github.com/otiai10/copy/stat_times_x.go +++ b/vendor/github.com/otiai10/copy/stat_times_x.go @@ -1,3 +1,4 @@ +//go:build plan9 || netbsd // +build plan9 netbsd package copy diff --git a/vendor/github.com/otiai10/copy/test_setup.go b/vendor/github.com/otiai10/copy/test_setup.go index a30edb7b70..f3c2d83cd2 100644 --- a/vendor/github.com/otiai10/copy/test_setup.go +++ b/vendor/github.com/otiai10/copy/test_setup.go @@ -1,4 +1,5 @@ -// +build !windows,!plan9,!netbsd,!aix,!illumos,!solaris +//go:build !windows && !plan9 && !netbsd && !aix && !illumos && !solaris && !js +// +build !windows,!plan9,!netbsd,!aix,!illumos,!solaris,!js package copy @@ -9,9 +10,10 @@ import ( ) func setup(m *testing.M) { + os.RemoveAll("test/data.copy") os.MkdirAll("test/data.copy", os.ModePerm) os.Symlink("test/data/case01", "test/data/case03/case01") - os.Chmod("test/data/case07/dir_0555", 0555) - os.Chmod("test/data/case07/file_0444", 0444) - syscall.Mkfifo("test/data/case11/foo/bar", 0555) + os.Chmod("test/data/case07/dir_0555", 0o555) + os.Chmod("test/data/case07/file_0444", 0o444) + syscall.Mkfifo("test/data/case11/foo/bar", 0o555) } diff --git a/vendor/github.com/otiai10/copy/test_setup_x.go b/vendor/github.com/otiai10/copy/test_setup_x.go index e4b44c10e0..fc56b7329e 100644 --- a/vendor/github.com/otiai10/copy/test_setup_x.go +++ b/vendor/github.com/otiai10/copy/test_setup_x.go @@ -1,4 +1,5 @@ -// +build windows plan9 netbsd aix illumos solaris +//go:build windows || plan9 || netbsd || aix || illumos || solaris || js +// +build windows plan9 netbsd aix illumos solaris js package copy diff --git a/vendor/modules.txt b/vendor/modules.txt index ed4496dfd0..fc750f343f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -419,22 +419,18 @@ github.com/liggitt/tabwriter # github.com/loft-sh/go-github-selfupdate v1.0.0 ## explicit; go 1.13 github.com/loft-sh/go-github-selfupdate/selfupdate -# github.com/loft-sh/loft-util v0.0.9-alpha -## explicit; go 1.19 -github.com/loft-sh/loft-util/pkg/command # github.com/loft-sh/notify v0.0.0-20210827094439-0720dcc7feee ## explicit; go 1.11 github.com/loft-sh/notify # github.com/loft-sh/programming-language-detection v0.0.5 ## explicit; go 1.20 github.com/loft-sh/programming-language-detection/pkg/detector -# github.com/loft-sh/utils v0.0.16 -## explicit; go 1.19 +# github.com/loft-sh/utils v0.0.30 +## explicit; go 1.24 github.com/loft-sh/utils/pkg/command github.com/loft-sh/utils/pkg/downloader github.com/loft-sh/utils/pkg/downloader/commands github.com/loft-sh/utils/pkg/extract -github.com/loft-sh/utils/pkg/log # github.com/mailru/easyjson v0.7.7 ## explicit; go 1.12 github.com/mailru/easyjson/buffer @@ -618,8 +614,8 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runtime-spec v1.2.1 ## explicit github.com/opencontainers/runtime-spec/specs-go -# github.com/otiai10/copy v1.7.0 -## explicit; go 1.14 +# github.com/otiai10/copy v1.11.0 +## explicit; go 1.18 github.com/otiai10/copy # github.com/peterbourgon/diskv v2.0.1+incompatible ## explicit @@ -1426,8 +1422,8 @@ k8s.io/utils/net k8s.io/utils/pointer k8s.io/utils/ptr k8s.io/utils/strings/slices -# mvdan.cc/sh/v3 v3.5.1 -## explicit; go 1.17 +# mvdan.cc/sh/v3 v3.6.0 +## explicit; go 1.18 mvdan.cc/sh/v3/expand mvdan.cc/sh/v3/fileutil mvdan.cc/sh/v3/interp diff --git a/vendor/mvdan.cc/sh/v3/expand/arith.go b/vendor/mvdan.cc/sh/v3/expand/arith.go index 1e48a709bc..3ce199ec65 100644 --- a/vendor/mvdan.cc/sh/v3/expand/arith.go +++ b/vendor/mvdan.cc/sh/v3/expand/arith.go @@ -6,6 +6,7 @@ package expand import ( "fmt" "strconv" + "strings" "mvdan.cc/sh/v3/syntax" ) @@ -92,7 +93,7 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { if err != nil { return 0, err } - return binArit(x.Op, left, right), nil + return binArit(x.Op, left, right) default: panic(fmt.Sprintf("unexpected arithm expr: %T", x)) } @@ -105,9 +106,9 @@ func oneIf(b bool) int { return 0 } -// atoi is just a shorthand for strconv.Atoi that ignores the error, -// just like shells do. +// atoi is like strconv.Atoi, but it ignores errors and trims whitespace. func atoi(s string) int { + s = strings.TrimSpace(s) n, _ := strconv.Atoi(s) return n } @@ -129,8 +130,14 @@ func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) { case syntax.MulAssgn: val *= arg case syntax.QuoAssgn: + if arg == 0 { + return 0, fmt.Errorf("division by zero") + } val /= arg case syntax.RemAssgn: + if arg == 0 { + return 0, fmt.Errorf("division by zero") + } val %= arg case syntax.AndAssgn: val &= arg @@ -161,48 +168,54 @@ func intPow(a, b int) int { return p } -func binArit(op syntax.BinAritOperator, x, y int) int { +func binArit(op syntax.BinAritOperator, x, y int) (int, error) { switch op { case syntax.Add: - return x + y + return x + y, nil case syntax.Sub: - return x - y + return x - y, nil case syntax.Mul: - return x * y + return x * y, nil case syntax.Quo: - return x / y + if y == 0 { + return 0, fmt.Errorf("division by zero") + } + return x / y, nil case syntax.Rem: - return x % y + if y == 0 { + return 0, fmt.Errorf("division by zero") + } + return x % y, nil case syntax.Pow: - return intPow(x, y) + return intPow(x, y), nil case syntax.Eql: - return oneIf(x == y) + return oneIf(x == y), nil case syntax.Gtr: - return oneIf(x > y) + return oneIf(x > y), nil case syntax.Lss: - return oneIf(x < y) + return oneIf(x < y), nil case syntax.Neq: - return oneIf(x != y) + return oneIf(x != y), nil case syntax.Leq: - return oneIf(x <= y) + return oneIf(x <= y), nil case syntax.Geq: - return oneIf(x >= y) + return oneIf(x >= y), nil case syntax.And: - return x & y + return x & y, nil case syntax.Or: - return x | y + return x | y, nil case syntax.Xor: - return x ^ y + return x ^ y, nil case syntax.Shr: - return x >> uint(y) + return x >> uint(y), nil case syntax.Shl: - return x << uint(y) + return x << uint(y), nil case syntax.AndArit: - return oneIf(x != 0 && y != 0) + return oneIf(x != 0 && y != 0), nil case syntax.OrArit: - return oneIf(x != 0 || y != 0) + return oneIf(x != 0 || y != 0), nil default: // syntax.Comma // x is executed but its result discarded - return y + return y, nil } } diff --git a/vendor/mvdan.cc/sh/v3/expand/expand.go b/vendor/mvdan.cc/sh/v3/expand/expand.go index 2619d846fd..a3152dce83 100644 --- a/vendor/mvdan.cc/sh/v3/expand/expand.go +++ b/vendor/mvdan.cc/sh/v3/expand/expand.go @@ -215,6 +215,24 @@ func Pattern(cfg *Config, word *syntax.Word) (string, error) { func Format(cfg *Config, format string, args []string) (string, int, error) { cfg = prepareConfig(cfg) buf := cfg.strBuilder() + + consumed, err := formatIntoBuffer(buf, format, args) + if err != nil { + return "", 0, err + } + + return buf.String(), consumed, err +} + +// Format expands a format string with a number of arguments, following the +// shell's format specifications. These include printf(1), among others. +// +// The resulting string is written to the provided buffer, and the number +// of arguments used is returned. +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func formatIntoBuffer(buf *bytes.Buffer, format string, args []string) (int, error) { var fmts []byte initialArgs := len(args) @@ -314,18 +332,26 @@ formatLoop: fmts = nil case '+', '-', ' ': if len(fmts) > 1 { - return "", 0, fmt.Errorf("invalid format char: %c", c) + return 0, fmt.Errorf("invalid format char: %c", c) } fmts = append(fmts, c) case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': fmts = append(fmts, c) - case 's', 'd', 'i', 'u', 'o', 'x': + case 's', 'b', 'd', 'i', 'u', 'o', 'x': arg := "" if len(args) > 0 { arg, args = args[0], args[1:] } - var farg interface{} = arg - if c != 's' { + var farg interface{} + if c == 'b' { + // Passing in nil for args ensures that % format + // strings aren't processed; only escape sequences + // will be handled. + _, err := formatIntoBuffer(buf, arg, nil) + if err != nil { + return 0, err + } + } else if c != 's' { n, _ := strconv.ParseInt(arg, 0, 0) if c == 'i' || c == 'd' { farg = int(n) @@ -335,12 +361,16 @@ formatLoop: if c == 'i' || c == 'u' { c = 'd' } + } else { + farg = arg + } + if farg != nil { + fmts = append(fmts, c) + fmt.Fprintf(buf, string(fmts), farg) } - fmts = append(fmts, c) - fmt.Fprintf(buf, string(fmts), farg) fmts = nil default: - return "", 0, fmt.Errorf("invalid format char: %c", c) + return 0, fmt.Errorf("invalid format char: %c", c) } case args != nil && c == '%': // if args == nil, we are not doing format @@ -351,9 +381,9 @@ formatLoop: } } if len(fmts) > 0 { - return "", 0, fmt.Errorf("missing format char") + return 0, fmt.Errorf("missing format char") } - return buf.String(), initialArgs - len(args), nil + return initialArgs - len(args), nil } func (cfg *Config) fieldJoin(parts []fieldPart) string { @@ -546,11 +576,22 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { curField = nil } splitAdd := func(val string) { - for i, field := range strings.FieldsFunc(val, cfg.ifsRune) { - if i > 0 { + fieldStart := -1 + for i, r := range val { + if cfg.ifsRune(r) { + if fieldStart >= 0 { // ending a field + curField = append(curField, fieldPart{val: val[fieldStart:i]}) + fieldStart = -1 + } flush() + } else { + if fieldStart < 0 { // starting a new field + fieldStart = i + } } - curField = append(curField, fieldPart{val: field}) + } + if fieldStart >= 0 { // ending a field without IFS + curField = append(curField, fieldPart{val: val[fieldStart:]}) } } for i, wp := range wps { @@ -636,6 +677,8 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { return nil, err } splitAdd(path) + case *syntax.ExtGlob: + return nil, fmt.Errorf("extended globbing is not supported") default: panic(fmt.Sprintf("unhandled word part: %T", x)) } @@ -648,31 +691,57 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } // quotedElemFields returns the list of elements resulting from a quoted -// parameter expansion if it was in the form of ${*}, ${@}, ${foo[*], ${foo[@]}, -// or ${!foo@}. +// parameter expansion that should be treated especially, like "${foo[@]}". func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { if pe == nil || pe.Length || pe.Width { return nil } + name := pe.Param.Value if pe.Excl { - if pe.Names == syntax.NamesPrefixWords { + switch pe.Names { + case syntax.NamesPrefixWords: // "${!prefix@}" return cfg.namesByPrefix(pe.Param.Value) + case syntax.NamesPrefix: // "${!prefix*}" + return nil + } + switch nodeLit(pe.Index) { + case "@": // "${!name[@]}" + switch vr := cfg.Env.Get(name); vr.Kind { + case Indexed: + keys := make([]string, 0, len(vr.Map)) + for key := range vr.List { + keys = append(keys, strconv.Itoa(key)) + } + return keys + case Associative: + keys := make([]string, 0, len(vr.Map)) + for key := range vr.Map { + keys = append(keys, key) + } + return keys + } } return nil } - name := pe.Param.Value switch name { - case "*": + case "*": // "${*}" return []string{cfg.ifsJoin(cfg.Env.Get(name).List)} - case "@": + case "@": // "${@}" return cfg.Env.Get(name).List } switch nodeLit(pe.Index) { - case "@": - if vr := cfg.Env.Get(name); vr.Kind == Indexed { + case "@": // "${name[@]}" + switch vr := cfg.Env.Get(name); vr.Kind { + case Indexed: return vr.List + case Associative: + elems := make([]string, 0, len(vr.Map)) + for _, elem := range vr.Map { + elems = append(elems, elem) + } + return elems } - case "*": + case "*": // "${name[*]}" if vr := cfg.Env.Get(name); vr.Kind == Indexed { return []string{cfg.ifsJoin(vr.List)} } @@ -845,21 +914,22 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { // If dir is not a directory, we keep the stack as-is and continue. newMatches = newMatches[:0] - newMatches, _ = cfg.globDir(base, dir, rxGlobStar, wantDir, newMatches) + newMatches, _ = cfg.globDir(base, dir, rxGlobStar, false, wantDir, newMatches) for i := len(newMatches) - 1; i >= 0; i-- { stack = append(stack, newMatches[i]) } } continue } - expr, err := pattern.Regexp(part, pattern.Filenames) + expr, err := pattern.Regexp(part, pattern.Filenames|pattern.EntireString) if err != nil { return nil, err } - rx := regexp.MustCompile("^" + expr + "$") + rx := regexp.MustCompile(expr) + matchHidden := part[0] == byte('.') var newMatches []string for _, dir := range matches { - newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) + newMatches, err = cfg.globDir(base, dir, rx, matchHidden, wantDir, newMatches) if err != nil { return nil, err } @@ -869,7 +939,7 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { return matches, nil } -func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) { +func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matchHidden bool, wantDir bool, matches []string) ([]string, error) { fullDir := dir if !filepath.IsAbs(dir) { fullDir = filepath.Join(base, dir) @@ -896,7 +966,7 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, ma // Not a symlink nor a directory. continue } - if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' { + if !matchHidden && name[0] == '.' { continue } if rx.MatchString(name) { diff --git a/vendor/mvdan.cc/sh/v3/expand/param.go b/vendor/mvdan.cc/sh/v3/expand/param.go index da77715512..bf0d23820e 100644 --- a/vendor/mvdan.cc/sh/v3/expand/param.go +++ b/vendor/mvdan.cc/sh/v3/expand/param.go @@ -160,18 +160,20 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { strs = cfg.namesByPrefix(pe.Param.Value) case orig.Kind == NameRef: strs = append(strs, orig.Str) - case vr.Kind == Indexed: + case pe.Index != nil && vr.Kind == Indexed: for i, e := range vr.List { if e != "" { strs = append(strs, strconv.Itoa(i)) } } - case vr.Kind == Associative: + case pe.Index != nil && vr.Kind == Associative: for k := range vr.Map { strs = append(strs, k) } - case !syntax.ValidName(str): + case vr.Kind == Unset: return "", fmt.Errorf("invalid indirect expansion") + case str == "": + return "", nil default: vr = cfg.Env.Get(str) strs = append(strs, vr.String()) diff --git a/vendor/mvdan.cc/sh/v3/fileutil/file.go b/vendor/mvdan.cc/sh/v3/fileutil/file.go index 629724892e..1ccf6fef2b 100644 --- a/vendor/mvdan.cc/sh/v3/fileutil/file.go +++ b/vendor/mvdan.cc/sh/v3/fileutil/file.go @@ -62,23 +62,7 @@ const ( // // Deprecated: prefer CouldBeScript2, which usually requires fewer syscalls. func CouldBeScript(info os.FileInfo) ScriptConfidence { - // TODO: once we drop support for Go 1.16, - // make use of this Go 1.17 API instead: - // return CouldBeScript2(fs.FileInfoToDirEntry(info)) - - name := info.Name() - switch { - case info.IsDir(), name[0] == '.': - return ConfNotScript - case info.Mode()&os.ModeSymlink != 0: - return ConfNotScript - case extRe.MatchString(name): - return ConfIsScript - case strings.IndexByte(name, '.') > 0: - return ConfNotScript // different extension - default: - return ConfIfShebang - } + return CouldBeScript2(fs.FileInfoToDirEntry(info)) } // CouldBeScript2 reports how likely a directory entry is to be a shell script. diff --git a/vendor/mvdan.cc/sh/v3/interp/api.go b/vendor/mvdan.cc/sh/v3/interp/api.go index 0648bd9db6..149c936ee7 100644 --- a/vendor/mvdan.cc/sh/v3/interp/api.go +++ b/vendor/mvdan.cc/sh/v3/interp/api.go @@ -4,6 +4,13 @@ // Package interp implements an interpreter that executes shell // programs. It aims to support POSIX, but its support is not complete // yet. It also supports some Bash features. +// +// The interpreter generally aims to behave like Bash, +// but it does not support all of its features. +// +// The interpreter currently aims to behave like a non-interactive shell, +// which is how most shells run scripts, and is more useful to machines. +// In the future, it may gain an option to behave like an interactive shell. package interp import ( @@ -185,6 +192,12 @@ func New(opts ...RunnerOption) (*Runner, error) { return nil, err } } + + // turn "on" the default Bash options + for i, opt := range bashOptsTable { + r.opts[len(shellOptsTable)+i] = opt.defaultState + } + // Set the default fallbacks, if necessary. if r.Env == nil { Env(nil)(r) @@ -274,7 +287,7 @@ func Params(args ...string) RunnerOption { value := fp.value() if value == "" && enable { for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } continue } @@ -288,7 +301,7 @@ func Params(args ...string) RunnerOption { } continue } - opt := r.optByName(value, false) + _, opt := r.optByName(value, false) if opt == nil { return fmt.Errorf("invalid option: %q", value) } @@ -366,28 +379,38 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption { } } -func (r *Runner) optByName(name string, bash bool) *bool { +// optByName returns the matching runner's option index and status +func (r *Runner) optByName(name string, bash bool) (index int, status *bool) { if bash { - for i, optName := range bashOptsTable { - if optName == name { - return &r.opts[len(shellOptsTable)+i] + for i, opt := range bashOptsTable { + if opt.name == name { + index = len(shellOptsTable) + i + return index, &r.opts[index] } } } for i, opt := range &shellOptsTable { if opt.name == name { - return &r.opts[i] + return i, &r.opts[i] } } - return nil + return 0, nil } type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool -var shellOptsTable = [...]struct { +type shellOpt struct { flag byte name string -}{ +} + +type bashOpt struct { + name string + defaultState bool // Bash's default value for this option + supported bool // whether we support the option's non-default state +} + +var shellOptsTable = [...]shellOpt{ // sorted alphabetically by name; use a space for the options // that have no flag form {'a', "allexport"}, @@ -399,11 +422,108 @@ var shellOptsTable = [...]struct { {' ', "pipefail"}, } -var bashOptsTable = [...]string{ - // sorted alphabetically by name - "expand_aliases", - "globstar", - "nullglob", +var bashOptsTable = [...]bashOpt{ + // supported options, sorted alphabetically by name + { + name: "expand_aliases", + defaultState: false, + supported: true, + }, + { + name: "globstar", + defaultState: false, + supported: true, + }, + { + name: "nullglob", + defaultState: false, + supported: true, + }, + // unsupported options, sorted alphabetically by name + {name: "assoc_expand_once"}, + {name: "autocd"}, + {name: "cdable_vars"}, + {name: "cdspell"}, + {name: "checkhash"}, + {name: "checkjobs"}, + { + name: "checkwinsize", + defaultState: true, + }, + { + name: "cmdhist", + defaultState: true, + }, + {name: "compat31"}, + {name: "compat32"}, + {name: "compat40"}, + {name: "compat41"}, + {name: "compat42"}, + {name: "compat44"}, + {name: "compat43"}, + {name: "compat44"}, + { + name: "complete_fullquote", + defaultState: true, + }, + {name: "direxpand"}, + {name: "dirspell"}, + {name: "dotglob"}, + {name: "execfail"}, + {name: "extdebug"}, + {name: "extglob"}, + { + name: "extquote", + defaultState: true, + }, + {name: "failglob"}, + { + name: "force_fignore", + defaultState: true, + }, + {name: "globasciiranges"}, + {name: "gnu_errfmt"}, + {name: "histappend"}, + {name: "histreedit"}, + {name: "histverify"}, + { + name: "hostcomplete", + defaultState: true, + }, + {name: "huponexit"}, + { + name: "inherit_errexit", + defaultState: true, + }, + { + name: "interactive_comments", + defaultState: true, + }, + {name: "lastpipe"}, + {name: "lithist"}, + {name: "localvar_inherit"}, + {name: "localvar_unset"}, + {name: "login_shell"}, + {name: "mailwarn"}, + {name: "no_empty_cmd_completion"}, + {name: "nocaseglob"}, + {name: "nocasematch"}, + { + name: "progcomp", + defaultState: true, + }, + {name: "progcomp_alias"}, + { + name: "promptvars", + defaultState: true, + }, + {name: "restricted_shell"}, + {name: "shift_verbose"}, + { + name: "sourcepath", + defaultState: true, + }, + {name: "xpg_echo"}, } // To access the shell options arrays without a linear search when we diff --git a/vendor/mvdan.cc/sh/v3/interp/builtin.go b/vendor/mvdan.cc/sh/v3/interp/builtin.go index f8161998ed..2f4a43ad2e 100644 --- a/vendor/mvdan.cc/sh/v3/interp/builtin.go +++ b/vendor/mvdan.cc/sh/v3/interp/builtin.go @@ -4,6 +4,7 @@ package interp import ( + "bufio" "bytes" "context" "errors" @@ -25,12 +26,14 @@ func isBuiltin(name string) bool { "wait", "builtin", "trap", "type", "source", ".", "command", "dirs", "pushd", "popd", "umask", "alias", "unalias", "fg", "bg", "getopts", "eval", "test", "[", "exec", - "return", "read", "shopt": + "return", "read", "mapfile", "readarray", "shopt": return true } return false } +// TODO: oneIf and atoi are duplicated in the expand package. + func oneIf(b bool) int { if b { return 1 @@ -38,9 +41,9 @@ func oneIf(b bool) int { return 0 } -// atoi is just a shorthand for strconv.Atoi that ignores the error, -// just like shells do. +// atoi is like strconv.Atoi, but it ignores errors and trims whitespace. func atoi(s string) int { + s = strings.TrimSpace(s) n, _ := strconv.Atoi(s) return n } @@ -667,29 +670,44 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } } args := fp.args() + bash := !posixOpts if len(args) == 0 { - if !posixOpts { - for i, name := range bashOptsTable { - r.printOptLine(name, r.opts[len(shellOptsTable)+i]) + if bash { + for i, opt := range bashOptsTable { + r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported) } break } for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } break } for _, arg := range args { - opt := r.optByName(arg, !posixOpts) + i, opt := r.optByName(arg, bash) if opt == nil { r.errf("shopt: invalid option name %q\n", arg) return 1 } + + var ( + bo *bashOpt + supported = true // default for shell options + ) + if bash { + bo = &bashOptsTable[i-len(shellOptsTable)] + supported = bo.supported + } + switch mode { case "-s", "-u": + if bash && !supported { + r.errf("shopt: invalid option name %q %q (%q not supported)\n", arg, r.optStatusText(bo.defaultState), r.optStatusText(!bo.defaultState)) + return 1 + } *opt = mode == "-s" default: // "" - r.printOptLine(arg, *opt) + r.printOptLine(arg, *opt, supported) } } r.updateExpandOpts() @@ -800,6 +818,64 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a return 2 } } + + case "readarray", "mapfile": + dropDelim := false + delim := "\n" + fp := flagParser{remaining: args} + for fp.more() { + switch flag := fp.flag(); flag { + case "-t": + // Remove the delim from each line read + dropDelim = true + case "-d": + if len(fp.remaining) == 0 { + r.errf("%s: -d: option requires an argument\n", name) + return 2 + } + delim = fp.value() + if delim == "" { + // Bash sets the delim to an ASCII NUL if provided with an empty + // string. + delim = "\x00" + } + default: + r.errf("%s: invalid option %q\n", name, flag) + return 2 + } + } + + args := fp.args() + var arrayName string + switch len(args) { + case 0: + arrayName = "MAPFILE" + case 1: + if !syntax.ValidName(args[0]) { + r.errf("%s: invalid identifier %q\n", name, args[0]) + return 2 + } + arrayName = args[0] + default: + r.errf("%s: Only one array name may be specified, %v\n", name, args) + return 2 + } + + var vr expand.Variable + vr.Kind = expand.Indexed + scanner := bufio.NewScanner(r.stdin) + scanner.Split(mapfileSplit(delim[0], dropDelim)) + for scanner.Scan() { + vr.List = append(vr.List, scanner.Text()) + } + if err := scanner.Err(); err != nil { + r.errf("%s: unable to read, %v", name, err) + return 2 + } + r.setVarInternal(arrayName, vr) + + return 0 + default: // "umask", "fg", "bg", panic(fmt.Sprintf("unhandled builtin: %s", name)) @@ -807,12 +883,37 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a return 0 } -func (r *Runner) printOptLine(name string, enabled bool) { - status := "off" - if enabled { - status = "on" +// mapfileSplit returns a suitable Split function for a bufio.Scanner, the code +// is mostly stolen from bufio.ScanLines. +func mapfileSplit(delim byte, dropDelim bool) func(data []byte, atEOF bool) (advance int, token []byte, err error) { + return func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, delim); i >= 0 { + // We have a full newline-terminated line. + if dropDelim { + return i + 1, data[0:i], nil + } else { + return i + 1, data[0 : i+1], nil + } + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil + } +} + +func (r *Runner) printOptLine(name string, enabled, supported bool) { + state := r.optStatusText(enabled) + if supported { + r.outf("%s\t%s\n", name, state) + return } - r.outf("%s\t%s\n", name, status) + r.outf("%s\t%s\t(%q not supported)\n", name, state, r.optStatusText(!enabled)) } func (r *Runner) readLine(raw bool) ([]byte, error) { @@ -987,3 +1088,11 @@ func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, d return opt, optarg, false } + +// optStatusText returns a shell option's status text display +func (r *Runner) optStatusText(status bool) string { + if status { + return "on" + } + return "off" +} diff --git a/vendor/mvdan.cc/sh/v3/interp/handler.go b/vendor/mvdan.cc/sh/v3/interp/handler.go index 881ed83b32..1bce7c8cf6 100644 --- a/vendor/mvdan.cc/sh/v3/interp/handler.go +++ b/vendor/mvdan.cc/sh/v3/interp/handler.go @@ -67,6 +67,10 @@ type HandlerContext struct { // Returning a non-nil error will halt the Runner. type CallHandlerFunc func(ctx context.Context, args []string) ([]string, error) +// TODO: consistently treat handler errors as non-fatal by default, +// but have an interface or API to specify fatal errors which should make +// the shell exit with a particular status code. + // ExecHandlerFunc is a handler which executes simple commands. // It is called for all CallExpr nodes where the first argument is neither a // declared function nor a builtin. diff --git a/vendor/mvdan.cc/sh/v3/interp/runner.go b/vendor/mvdan.cc/sh/v3/interp/runner.go index 13168aa958..405acda5c3 100644 --- a/vendor/mvdan.cc/sh/v3/interp/runner.go +++ b/vendor/mvdan.cc/sh/v3/interp/runner.go @@ -6,6 +6,7 @@ package interp import ( "bytes" "context" + "errors" "fmt" "io" "math" @@ -155,7 +156,19 @@ func (r *Runner) updateExpandOpts() { func (r *Runner) expandErr(err error) { if err != nil { - r.errf("%v\n", err) + errMsg := err.Error() + fmt.Fprintln(r.stderr, errMsg) + switch { + case errors.As(err, &expand.UnsetParameterError{}): + case errMsg == "invalid indirect expansion": + // TODO: These errors are treated as fatal by bash. + // Make the error type reflect that. + case strings.HasSuffix(errMsg, "not supported"): + // TODO: This "has suffix" is a temporary measure until the expand + // package supports all syntax nodes like extended globbing. + default: + return // other cases do not exit + } r.exitShell(context.TODO(), 1) } } @@ -298,7 +311,7 @@ func (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) { // part of && or || lists // preceded by ! r.exitShell(ctx, r.exit) - } else if r.exit != 0 { + } else if r.exit != 0 && !r.noErrExit { r.trapCallback(ctx, r.callbackErr, "error") } if !r.keepRedirs { @@ -705,11 +718,11 @@ func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign { } func match(pat, name string) bool { - expr, err := pattern.Regexp(pat, 0) + expr, err := pattern.Regexp(pat, pattern.EntireString) if err != nil { return false } - rx := regexp.MustCompile("(?m)^" + expr + "$") + rx := regexp.MustCompile(expr) return rx.MatchString(name) } diff --git a/vendor/mvdan.cc/sh/v3/interp/test.go b/vendor/mvdan.cc/sh/v3/interp/test.go index fb45699c4f..f27db8670e 100644 --- a/vendor/mvdan.cc/sh/v3/interp/test.go +++ b/vendor/mvdan.cc/sh/v3/interp/test.go @@ -190,7 +190,7 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) case syntax.TsNempStr: return x != "" case syntax.TsOptSet: - if opt := r.optByName(x, false); opt != nil { + if _, opt := r.optByName(x, false); opt != nil { return *opt } return false diff --git a/vendor/mvdan.cc/sh/v3/interp/trace.go b/vendor/mvdan.cc/sh/v3/interp/trace.go index 08b6eafceb..dbf5087eb2 100644 --- a/vendor/mvdan.cc/sh/v3/interp/trace.go +++ b/vendor/mvdan.cc/sh/v3/interp/trace.go @@ -14,7 +14,7 @@ import ( type tracer struct { buf bytes.Buffer printer *syntax.Printer - stdout io.Writer + output io.Writer needsPlus bool } @@ -25,7 +25,7 @@ func (r *Runner) tracer() *tracer { return &tracer{ printer: syntax.NewPrinter(), - stdout: r.stdout, + output: r.stderr, needsPlus: true, } } @@ -74,7 +74,7 @@ func (t *tracer) flush() { return } - t.stdout.Write(t.buf.Bytes()) + t.output.Write(t.buf.Bytes()) t.buf.Reset() } diff --git a/vendor/mvdan.cc/sh/v3/pattern/pattern.go b/vendor/mvdan.cc/sh/v3/pattern/pattern.go index fd80f71721..bde1ca7176 100644 --- a/vendor/mvdan.cc/sh/v3/pattern/pattern.go +++ b/vendor/mvdan.cc/sh/v3/pattern/pattern.go @@ -30,9 +30,10 @@ func (e SyntaxError) Error() string { return e.msg } func (e SyntaxError) Unwrap() error { return e.err } const ( - Shortest Mode = 1 << iota // prefer the shortest match. - Filenames // "*" and "?" don't match slashes; only "**" does - Braces // support "{a,b}" and "{1..4}" + Shortest Mode = 1 << iota // prefer the shortest match. + Filenames // "*" and "?" don't match slashes; only "**" does + Braces // support "{a,b}" and "{1..4}" + EntireString // match the entire string using ^$ delimiters ) var numRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`) @@ -59,11 +60,17 @@ noopLoop: break noopLoop } } - if !any { // short-cut without a string copy + if !any && mode&EntireString == 0 { // short-cut without a string copy return pat, nil } closingBraces := []int{} var buf bytes.Buffer + // Enable matching `\n` with the `.` metacharacter as globs match `\n` + buf.WriteString("(?s)") + dotMeta := false + if mode&EntireString != 0 { + buf.WriteString("^") + } writeLoop: for i := 0; i < len(pat); i++ { switch c := pat[i]; c { @@ -72,8 +79,10 @@ writeLoop: if i++; i < len(pat) && pat[i] == '*' { if i++; i < len(pat) && pat[i] == '/' { buf.WriteString("(.*/|)") + dotMeta = true } else { buf.WriteString(".*") + dotMeta = true i-- } } else { @@ -82,6 +91,7 @@ writeLoop: } } else { buf.WriteString(".*") + dotMeta = true } if mode&Shortest != 0 { buf.WriteByte('?') @@ -91,6 +101,7 @@ writeLoop: buf.WriteString("[^/]") } else { buf.WriteByte('.') + dotMeta = true } case '\\': if i++; i >= len(pat) { @@ -228,6 +239,13 @@ writeLoop: } } } + if mode&EntireString != 0 { + buf.WriteString("$") + } + // No `.` metacharacters were used, so don't return the flag. + if !dotMeta { + return string(buf.Bytes()[4:]), nil + } return buf.String(), nil } diff --git a/vendor/mvdan.cc/sh/v3/syntax/lexer.go b/vendor/mvdan.cc/sh/v3/syntax/lexer.go index 133cc00d38..583abb61c8 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/lexer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/lexer.go @@ -303,7 +303,7 @@ skipSpace: p.advanceLitNone(r) } case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { switch r { case '?': p.tok = globQuest @@ -359,26 +359,35 @@ skipSpace: } } -// tokenizeGlob determines whether the expression should be tokenized as a glob literal -func (p *Parser) tokenizeGlob() bool { +// extendedGlob determines whether we're parsing a Bash extended globbing expression. +// For example, whether `*` or `@` are followed by `(` to form `@(foo)`. +func (p *Parser) extendedGlob() bool { if p.val == "function" { return false } - // NOTE: empty pattern list is a valid globbing syntax, eg @() - // but we'll operate on the "likelihood" that it is a function; - // only tokenize if its a non-empty pattern list - if p.peekBytes("()") { - return false + if p.peekByte('(') { + // NOTE: empty pattern list is a valid globbing syntax like `@()`, + // but we'll operate on the "likelihood" that it is a function; + // only tokenize if its a non-empty pattern list. + // We do this after peeking for just one byte, so that the input `echo *` + // followed by a newline does not hang an interactive shell parser until + // another byte is input. + if p.peekBytes("()") { + return false + } + return true } - return p.peekByte('(') + return false } func (p *Parser) peekBytes(s string) bool { - for p.bsp+(len(p.bs)-1) >= len(p.bs) { + peekEnd := p.bsp + len(s) + // TODO: This should loop for slow readers, e.g. those providing one byte at + // a time. Use a loop and test it with testing/iotest.OneByteReader. + if peekEnd > len(p.bs) { p.fill() } - bw := p.bsp + len(s) - return bw <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:bw], []byte(s)) + return peekEnd <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:peekEnd], []byte(s)) } func (p *Parser) peekByte(b byte) bool { @@ -503,7 +512,7 @@ func (p *Parser) regToken(r rune) token { if r = p.rune(); r == '-' { p.rune() return dashHdoc - } else if r == '<' && p.lang != LangPOSIX { + } else if r == '<' { p.rune() return wordHdoc } @@ -813,7 +822,7 @@ func (p *Parser) endLit() (s string) { if p.r == utf8.RuneSelf || p.r == escNewl { s = string(p.litBs) } else { - s = string(p.litBs[:len(p.litBs)-int(p.w)]) + s = string(p.litBs[:len(p.litBs)-p.w]) } p.litBs = nil return @@ -917,7 +926,7 @@ loop: tok = _Lit break loop case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { tok = _Lit break loop } @@ -1068,7 +1077,7 @@ func (p *Parser) quotedHdocWord() *Word { if val == "" { return nil } - return p.word(p.wps(p.lit(pos, val))) + return p.wordOne(p.lit(pos, val)) } } } diff --git a/vendor/mvdan.cc/sh/v3/syntax/nodes.go b/vendor/mvdan.cc/sh/v3/syntax/nodes.go index 32518ec877..a43021f7f7 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/nodes.go +++ b/vendor/mvdan.cc/sh/v3/syntax/nodes.go @@ -138,8 +138,9 @@ func (p Pos) String() string { return b.String() } -// IsValid reports whether the position is valid. All positions in nodes -// returned by Parse are valid. +// IsValid reports whether the position contains useful position information. +// Some positions returned via Parse may be invalid: for example, Stmt.Semicolon +// will only be valid if a statement contained a closing token such as ';'. func (p Pos) IsValid() bool { return p != Pos{} } // After reports whether the position p is after p2. It is a more expressive diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser.go b/vendor/mvdan.cc/sh/v3/syntax/parser.go index 21993b1e35..86e7274c2c 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser.go @@ -235,16 +235,16 @@ func (w *wrappedReader) Read(p []byte) (n int, err error) { // // One can imagine a simple interactive shell implementation as follows: // -// fmt.Fprintf(os.Stdout, "$ ") -// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { -// if parser.Incomplete() { -// fmt.Fprintf(os.Stdout, "> ") -// return true -// } -// run(stmts) -// fmt.Fprintf(os.Stdout, "$ ") -// return true -// } +// fmt.Fprintf(os.Stdout, "$ ") +// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { +// if parser.Incomplete() { +// fmt.Fprintf(os.Stdout, "> ") +// return true +// } +// run(stmts) +// fmt.Fprintf(os.Stdout, "$ ") +// return true +// } // // If the callback function returns false, parsing is stopped and the function // is not called again. @@ -400,12 +400,10 @@ type Parser struct { accComs []Comment curComs *[]Comment - litBatch []Lit - wordBatch []Word - wpsBatch []WordPart - stmtBatch []Stmt - stListBatch []*Stmt - callBatch []callAlloc + litBatch []Lit + wordBatch []wordAlloc + stmtBatch []Stmt + callBatch []callAlloc readBuf [bufSize]byte litBuf [bufSize]byte @@ -440,6 +438,10 @@ func (p *Parser) reset() { p.parsingDoc = false p.openBquotes, p.buriedBquotes = 0, 0 p.accComs, p.curComs = nil, &p.accComs + p.litBatch = nil + p.wordBatch = nil + p.stmtBatch = nil + p.callBatch = nil } func (p *Parser) nextPos() Pos { @@ -451,12 +453,12 @@ func (p *Parser) nextPos() Pos { if !p.colOverflow { col = uint(p.col) } - return NewPos(uint(p.offs+p.bsp-int(p.w)), line, col) + return NewPos(uint(p.offs+p.bsp-p.w), line, col) } func (p *Parser) lit(pos Pos, val string) *Lit { if len(p.litBatch) == 0 { - p.litBatch = make([]Lit, 128) + p.litBatch = make([]Lit, 64) } l := &p.litBatch[0] p.litBatch = p.litBatch[1:] @@ -466,29 +468,37 @@ func (p *Parser) lit(pos Pos, val string) *Lit { return l } -func (p *Parser) word(parts []WordPart) *Word { +type wordAlloc struct { + word Word + parts [1]WordPart +} + +func (p *Parser) wordAnyNumber() *Word { if len(p.wordBatch) == 0 { - p.wordBatch = make([]Word, 64) + p.wordBatch = make([]wordAlloc, 32) } - w := &p.wordBatch[0] + alloc := &p.wordBatch[0] p.wordBatch = p.wordBatch[1:] - w.Parts = parts + w := &alloc.word + w.Parts = p.wordParts(alloc.parts[:0]) return w } -func (p *Parser) wps(wp WordPart) []WordPart { - if len(p.wpsBatch) == 0 { - p.wpsBatch = make([]WordPart, 64) +func (p *Parser) wordOne(part WordPart) *Word { + if len(p.wordBatch) == 0 { + p.wordBatch = make([]wordAlloc, 32) } - wps := p.wpsBatch[:1:1] - p.wpsBatch = p.wpsBatch[1:] - wps[0] = wp - return wps + alloc := &p.wordBatch[0] + p.wordBatch = p.wordBatch[1:] + w := &alloc.word + w.Parts = alloc.parts[:1] + w.Parts[0] = part + return w } func (p *Parser) stmt(pos Pos) *Stmt { if len(p.stmtBatch) == 0 { - p.stmtBatch = make([]Stmt, 64) + p.stmtBatch = make([]Stmt, 32) } s := &p.stmtBatch[0] p.stmtBatch = p.stmtBatch[1:] @@ -496,15 +506,6 @@ func (p *Parser) stmt(pos Pos) *Stmt { return s } -func (p *Parser) stList() []*Stmt { - if len(p.stListBatch) == 0 { - p.stListBatch = make([]*Stmt, 256) - } - stmts := p.stListBatch[:0:4] - p.stListBatch = p.stListBatch[4:] - return stmts -} - type callAlloc struct { ce CallExpr ws [4]*Word @@ -573,39 +574,37 @@ func (p *Parser) postNested(s saveState) { } func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) { - var buf bytes.Buffer + buf := make([]byte, 0, 4) didUnquote := false for _, wp := range w.Parts { - if p.unquotedWordPart(&buf, wp, false) { - didUnquote = true - } + buf, didUnquote = p.unquotedWordPart(buf, wp, false) } - return buf.Bytes(), didUnquote + return buf, didUnquote } -func (p *Parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (quoted bool) { +func (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool) (_ []byte, quoted bool) { switch x := wp.(type) { case *Lit: for i := 0; i < len(x.Value); i++ { if b := x.Value[i]; b == '\\' && !quotes { if i++; i < len(x.Value) { - buf.WriteByte(x.Value[i]) + buf = append(buf, x.Value[i]) } quoted = true } else { - buf.WriteByte(b) + buf = append(buf, b) } } case *SglQuoted: - buf.WriteString(x.Value) + buf = append(buf, []byte(x.Value)...) quoted = true case *DblQuoted: for _, wp2 := range x.Parts { - p.unquotedWordPart(buf, wp2, true) + buf, _ = p.unquotedWordPart(buf, wp2, true) } quoted = true } - return + return buf, quoted } func (p *Parser) doHeredocs() { @@ -645,7 +644,7 @@ func (p *Parser) doHeredocs() { // should. Look into it. l := p.lit(p.nextPos(), "") if r.Hdoc == nil { - r.Hdoc = p.word(p.wps(l)) + r.Hdoc = p.wordOne(l) } else { r.Hdoc.Parts = append(r.Hdoc.Parts, l) } @@ -922,9 +921,6 @@ func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { var stmts []*Stmt var last []Comment fn := func(s *Stmt) bool { - if stmts == nil { - stmts = p.stList() - } stmts = append(stmts, s) return true } @@ -968,8 +964,8 @@ func (p *Parser) invalidStmtStart() { } func (p *Parser) getWord() *Word { - if parts := p.wordParts(); len(parts) > 0 && p.err == nil { - return p.word(parts) + if w := p.wordAnyNumber(); len(w.Parts) > 0 && p.err == nil { + return w } return nil } @@ -984,19 +980,18 @@ func (p *Parser) getLit() *Lit { return nil } -func (p *Parser) wordParts() (wps []WordPart) { +func (p *Parser) wordParts(wps []WordPart) []WordPart { for { n := p.wordPart() if n == nil { - return - } - if wps == nil { - wps = p.wps(n) - } else { - wps = append(wps, n) + if len(wps) == 0 { + return nil // normalize empty lists into nil + } + return wps } + wps = append(wps, n) if p.spaced { - return + return wps } } } @@ -1009,7 +1004,7 @@ func (p *Parser) ensureNoNested() { func (p *Parser) wordPart() WordPart { switch p.tok { - case _Lit, _LitWord: + case _Lit, _LitWord, _LitRedir: l := p.lit(p.pos, p.val) p.next() return l @@ -1214,11 +1209,17 @@ func (p *Parser) wordPart() WordPart { } func (p *Parser) dblQuoted() *DblQuoted { - q := &DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote} + alloc := &struct { + quoted DblQuoted + parts [1]WordPart + }{ + quoted: DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote}, + } + q := &alloc.quoted old := p.quote p.quote = dblQuotes p.next() - q.Parts = p.wordParts() + q.Parts = p.wordParts(alloc.parts[:0]) p.quote = old q.Right = p.pos if !p.got(dblQuote) { @@ -1263,9 +1264,6 @@ func (p *Parser) paramExp() *ParamExp { } case exclMark: if paramNameOp(p.r) { - if p.lang == LangPOSIX { - p.langErr(p.pos, "${!foo}", LangBash, LangMirBSDKorn) - } pe.Excl = true p.next() } @@ -1297,6 +1295,9 @@ func (p *Parser) paramExp() *ParamExp { case _Lit, _LitWord: p.curErr("%s cannot be followed by a word", op) case rightBrace: + if pe.Excl && p.lang == LangPOSIX { + p.posErr(pe.Pos(), `"${!foo}" is a bash/mksh feature`) + } pe.Rbrace = p.pos p.quote = old p.next() @@ -1367,6 +1368,9 @@ func (p *Parser) paramExp() *ParamExp { case p.tok == star && !pe.Excl: p.curErr("not a valid parameter expansion operator: %v", p.tok) case pe.Excl && p.r == '}': + if !p.lang.isBash() { + p.posErr(pe.Pos(), `"${!foo`+p.tok.String()+`}" is a bash feature`) + } pe.Names = ParNamesOperator(p.tok) p.next() default: @@ -1500,7 +1504,7 @@ func (p *Parser) getAssign(needEqual bool) *Assign { left := p.lit(posAddCol(p.pos, 1), p.val[p.eqlOffs+1:]) if left.Value != "" { left.ValuePos = posAddCol(left.ValuePos, p.eqlOffs) - as.Value = p.word(p.wps(left)) + as.Value = p.wordOne(left) } p.next() } else { // foo[x]=bar @@ -1643,6 +1647,11 @@ func (p *Parser) doRedirect(s *Stmt) { } p.doHeredocs() } + case WordHdoc: + if p.lang == LangPOSIX { + p.langErr(r.OpPos, "herestrings", LangBash, LangMirBSDKorn) + } + fallthrough default: r.Word = p.followWordTok(token(r.Op), r.OpPos) } @@ -1751,8 +1760,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } case "]]": if p.lang != LangPOSIX { - p.curErr(`%q can only be used to close a test`, - p.val) + p.curErr(`%q can only be used to close a test`, p.val) } case "let": if p.lang != LangPOSIX { @@ -1763,7 +1771,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.bashFuncDecl(s) } case "declare": - if p.lang.isBash() { + if p.lang.isBash() { // Note that mksh lacks this one. p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": @@ -1775,7 +1783,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.timeClause(s) } case "coproc": - if p.lang.isBash() { + if p.lang.isBash() { // Note that mksh lacks this one. p.coprocClause(s) } case "select": @@ -1802,7 +1810,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } p.funcDecl(s, name, name.ValuePos, true) } else { - p.callExpr(s, p.word(p.wps(name)), false) + p.callExpr(s, p.wordOne(name), false) } case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: @@ -1820,7 +1828,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.callExpr(s, nil, true) break } - w := p.word(p.wordParts()) + w := p.wordAnyNumber() if p.got(leftParen) { p.posErr(w.Pos(), "invalid func name") } @@ -2104,18 +2112,28 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { ci.Op = CaseOperator(p.tok) p.next() p.got(_Newl) + + // Split the comments: + // + // case x in + // a) + // foo + // ;; + // # comment for a + // # comment for b + // b) + // [...] split := len(p.accComs) - if p.tok == _LitWord && p.val != stop { - for i := len(p.accComs) - 1; i >= 0; i-- { - c := p.accComs[i] - if c.Pos().Col() != p.pos.Col() { - break - } - split = i + for i := len(p.accComs) - 1; i >= 0; i-- { + c := p.accComs[i] + if c.Pos().Col() != p.pos.Col() { + break } + split = i } ci.Comments = append(ci.Comments, p.accComs[:split]...) p.accComs = p.accComs[split:] + items = append(items, ci) } return @@ -2402,16 +2420,14 @@ loop: ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } - ce.Args = append(ce.Args, p.word( - p.wps(p.lit(p.pos, p.val)), - )) + ce.Args = append(ce.Args, p.wordOne(p.lit(p.pos, p.val))) p.next() case _Lit: if len(ce.Args) == 0 && p.hasValidIdent() { ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } - ce.Args = append(ce.Args, p.word(p.wordParts())) + ce.Args = append(ce.Args, p.wordAnyNumber()) case bckQuote: if p.backquoteEnd() { break loop @@ -2420,7 +2436,7 @@ loop: case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: - ce.Args = append(ce.Args, p.word(p.wordParts())) + ce.Args = append(ce.Args, p.wordAnyNumber()) case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: p.doRedirect(s) @@ -2432,6 +2448,12 @@ loop: } fallthrough default: + // Note that we'll only keep the first error that happens. + if len(ce.Args) > 0 { + if cmd := ce.Args[0].Lit(); p.lang == LangPOSIX && isBashCompoundCommand(_LitWord, cmd) { + p.curErr("the %q builtin exists in bash; tried parsing as posix", cmd) + } + } p.curErr("a command can only contain words and redirects; encountered %s", p.tok) } } diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go index 0021c62d7c..a6d6a951f8 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go @@ -189,12 +189,12 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { case _LitWord: l := p.getLit() if p.tok != leftBrack { - x = p.word(p.wps(l)) + x = p.wordOne(l) break } pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l} pe.Index = p.eitherIndex() - x = p.word(p.wps(pe)) + x = p.wordOne(pe) case bckQuote: if p.quote == arithmExprLet && p.openBquotes > 0 { return nil diff --git a/vendor/mvdan.cc/sh/v3/syntax/printer.go b/vendor/mvdan.cc/sh/v3/syntax/printer.go index 7dc183a024..6626b36392 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/printer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/printer.go @@ -313,8 +313,10 @@ func (p *Printer) wantsNewline(pos Pos) bool { // We must have a newline here. return true } - if p.singleLine { - // The newline is optional, and singleLine turns it off. + if p.singleLine && len(p.pendingComments) == 0 { + // The newline is optional, and singleLine skips it. + // Don't skip if there are any pending comments, + // as that might move them further down to the wrong place. return false } // THe newline is optional, and we want it via either wantNewline or via @@ -359,7 +361,7 @@ func (p *Printer) semiOrNewl(s string, pos Pos) { if !p.minify { p.space() } - p.line = pos.Line() + p.advanceLine(pos.Line()) } p.WriteString(s) p.wantSpace = spaceRequired @@ -415,14 +417,19 @@ func (p *Printer) indent() { // TODO(mvdan): add an indent call at the end of newline? +// newline prints one newline and advances p.line to pos.Line(). func (p *Printer) newline(pos Pos) { p.flushHeredocs() p.flushComments() p.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.mustNewline = false, false - if p.line < pos.Line() { - p.line++ + p.advanceLine(pos.Line()) +} + +func (p *Printer) advanceLine(line uint) { + if p.line < line { + p.line = line } } @@ -486,7 +493,7 @@ func (p *Printer) flushHeredocs() { if r.Hdoc != nil { // Overwrite p.line, since printing r.Word again can set // p.line to the beginning of the heredoc again. - p.line = r.Hdoc.End().Line() + p.advanceLine(r.Hdoc.End().Line()) } p.wantSpace = spaceNotRequired } @@ -495,6 +502,8 @@ func (p *Printer) flushHeredocs() { p.mustNewline = true } +// newline prints between zero and two newlines. +// If any newlines are printed, it advances p.line to pos.Line(). func (p *Printer) newlines(pos Pos) { if p.firstLine && len(p.pendingComments) == 0 { p.firstLine = false @@ -503,14 +512,17 @@ func (p *Printer) newlines(pos Pos) { if !p.wantsNewline(pos) { return } - p.newline(pos) - if pos.Line() > p.line { - if !p.minify { - // preserve single empty lines - p.WriteByte('\n') - } - p.line++ + p.flushHeredocs() + p.flushComments() + p.WriteByte('\n') + p.wantSpace = spaceWritten + p.wantNewline, p.mustNewline = false, false + + l := pos.Line() + if l > p.line+1 && !p.minify { + p.WriteByte('\n') // preserve single empty lines } + p.advanceLine(l) p.indent() } @@ -567,9 +579,7 @@ func (p *Printer) flushComments() { p.space() } // don't go back one line, which may happen in some edge cases - if p.line < cline { - p.line = cline - } + p.advanceLine(cline) p.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true @@ -593,6 +603,14 @@ func (p *Printer) comments(comments ...Comment) { } func (p *Printer) wordParts(wps []WordPart, quoted bool) { + // We disallow unquoted escaped newlines between word parts below. + // However, we want to allow a leading escaped newline for cases such as: + // + // foo <<< \ + // "bar baz" + if !quoted && !p.singleLine && wps[0].Pos().Line() > p.line { + p.bslashNewl() + } for i, wp := range wps { var next WordPart if i+1 < len(wps) { @@ -609,7 +627,7 @@ func (p *Printer) wordParts(wps []WordPart, quoted bool) { p.line++ } p.wordPart(wp, next) - p.line = wp.End().Line() + p.advanceLine(wp.End().Line()) } } @@ -624,11 +642,11 @@ func (p *Printer) wordPart(wp, next WordPart) { p.WriteByte('\'') p.writeLit(x.Value) p.WriteByte('\'') - p.line = x.End().Line() + p.advanceLine(x.End().Line()) case *DblQuoted: p.dblQuoted(x) case *CmdSubst: - p.line = x.Pos().Line() + p.advanceLine(x.Pos().Line()) switch { case x.TempFile: p.WriteString("${") @@ -855,7 +873,7 @@ func (p *Printer) testExpr(expr TestExpr) { } func (p *Printer) testExprSameLine(expr TestExpr) { - p.line = expr.Pos().Line() + p.advanceLine(expr.Pos().Line()) switch x := expr.(type) { case *Word: p.word(x) @@ -1038,7 +1056,7 @@ func (p *Printer) stmt(s *Stmt) { } func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { - p.line = cmd.Pos().Line() + p.advanceLine(cmd.Pos().Line()) p.spacePad(cmd.Pos()) switch x := cmd.(type) { case *CallExpr: @@ -1079,11 +1097,22 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.ifClause(x, false) case *Subshell: p.WriteByte('(') - if len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) { + stmts := x.Stmts + if len(stmts) > 0 && startsWithLparen(stmts[0]) { p.wantSpace = spaceRequired + // Add a space between nested parentheses if we're printing them in a single line, + // to avoid the ambiguity between `((` and `( (`. + if (x.Lparen.Line() != stmts[0].Pos().Line() || len(stmts) > 1) && !p.singleLine { + p.wantSpace = spaceNotRequired + + if p.minify { + p.mustNewline = true + } + } } else { p.wantSpace = spaceNotRequired } + p.spacePad(stmtsPos(x.Stmts, x.Last)) p.nestedStmts(x.Stmts, x.Last, x.Rparen) p.wantSpace = spaceNotRequired @@ -1114,7 +1143,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { if p.minify || p.singleLine || x.Y.Pos().Line() <= p.line { // leave p.nestedBinary untouched p.spacedToken(x.Op.String(), x.OpPos) - p.line = x.Y.Pos().Line() + p.advanceLine(x.Y.Pos().Line()) p.stmt(x.Y) break } @@ -1137,12 +1166,12 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { } } else { p.spacedToken(x.Op.String(), x.OpPos) - p.line = x.OpPos.Line() + p.advanceLine(x.OpPos.Line()) p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } - p.line = x.Y.Pos().Line() + p.advanceLine(x.Y.Pos().Line()) _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) p.stmt(x.Y) if indent { @@ -1163,13 +1192,14 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { } else if !x.Parens || !p.minify { p.space() } - p.line = x.Body.Pos().Line() + p.advanceLine(x.Body.Pos().Line()) p.comments(x.Body.Comments...) p.stmt(x.Body) case *CaseClause: p.WriteString("case ") p.word(x.Word) p.WriteString(" in") + p.advanceLine(x.In.Line()) p.wantSpace = spaceRequired if p.swtCaseIndent { p.incLevel() @@ -1209,6 +1239,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.wantNewline = true } p.spacedToken(ci.Op.String(), ci.OpPos) + p.advanceLine(ci.OpPos.Line()) // avoid ; directly after tokens like ;; p.wroteSemi = true } @@ -1338,10 +1369,10 @@ func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { // statement. p.comments(c) } - if !p.minify || p.wantSpace == spaceRequired { + if p.mustNewline || !p.minify || p.wantSpace == spaceRequired { p.newlines(pos) } - p.line = pos.Line() + p.advanceLine(pos.Line()) p.comments(midComs...) p.stmt(s) p.comments(endComs...) @@ -1401,11 +1432,8 @@ func (p *Printer) assigns(assigns []*Assign) { // Ensure we don't use an escaped newline after '=', // because that can result in indentation, thus // splitting "foo=bar" into "foo= bar". - p.line = a.Value.Pos().Line() - // Similar to the above, we want to print the word as if it were - // quoted, as otherwise escaped newlines could split + p.advanceLine(a.Value.Pos().Line()) p.word(a.Value) - // p.wordParts(a.Value.Parts, true) } else if a.Array != nil { p.wantSpace = spaceNotRequired p.WriteByte('(') diff --git a/vendor/mvdan.cc/sh/v3/syntax/simplify.go b/vendor/mvdan.cc/sh/v3/syntax/simplify.go index 6f245918e9..5a93966e25 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/simplify.go +++ b/vendor/mvdan.cc/sh/v3/syntax/simplify.go @@ -10,12 +10,13 @@ import "bytes" // // The changes currently applied are: // -// Remove clearly useless parentheses $(( (expr) )) -// Remove dollars from vars in exprs (($var)) -// Remove duplicate subshells $( (stmts) ) -// Remove redundant quotes [[ "$var" == str ]] -// Merge negations with unary operators [[ ! -n $var ]] -// Use single quotes to shorten literals "\$foo" +// Remove clearly useless parentheses $(( (expr) )) +// Remove dollars from vars in exprs (($var)) +// Remove duplicate subshells $( (stmts) ) +// Remove redundant quotes [[ "$var" == str ]] +// Merge negations with unary operators [[ ! -n $var ]] +// Use single quotes to shorten literals "\$foo" +// Remove redundant param expansion colons ${foo:-} func Simplify(n Node) bool { s := simplifier{} Walk(n, s.visit) @@ -37,6 +38,10 @@ func (s *simplifier) visit(node Node) bool { x.Index = s.removeParensArithm(x.Index) // don't inline params - same as above. + if x.Exp != nil && x.Exp.Op == DefaultUnsetOrNull && x.Exp.Word == nil { + s.modified = true + x.Exp.Op = DefaultUnset + } if x.Slice == nil { break } From dfd6a825e63a7fe1d92435b8f31e76841c967ec1 Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 17:44:03 -0700 Subject: [PATCH 2/3] Update test and build script for helm v4 Signed-off-by: Ryan Swanson --- hack/build-all.bash | 4 ++-- pkg/devspace/pipeline/engine/engine_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/build-all.bash b/hack/build-all.bash index c537502347..d6741e2576 100755 --- a/hack/build-all.bash +++ b/hack/build-all.bash @@ -41,9 +41,9 @@ fi # Create the release directory mkdir -p "${DEVSPACE_ROOT}/release" -# Install Helm 3 +# Install Helm 4 echo "Installing helm" -curl -s https://get.helm.sh/helm-v3.3.4-darwin-amd64.tar.gz > helm3.tar.gz && tar -zxvf helm3.tar.gz darwin-amd64/helm && chmod +x darwin-amd64/helm +curl -s https://get.helm.sh/helm-v4.0.4-darwin-amd64.tar.gz > helm4.tar.gz && tar -zxvf helm4.tar.gz darwin-amd64/helm && chmod +x darwin-amd64/helm # Pull the component chart COMPONENT_CHART_VERSION=$(cat pkg/devspace/deploy/deployer/helm/client.go | grep 'Version: "' | sed -nE 's/[^"]+"(.+)",\s*/\1/p') diff --git a/pkg/devspace/pipeline/engine/engine_test.go b/pkg/devspace/pipeline/engine/engine_test.go index ea535feaaf..20fecc32e2 100644 --- a/pkg/devspace/pipeline/engine/engine_test.go +++ b/pkg/devspace/pipeline/engine/engine_test.go @@ -132,5 +132,5 @@ func TestHelmDownload(t *testing.T) { if err != nil { t.Fatal(err) } - assert.Assert(t, strings.Contains(stdout1.String(), `Version:"v3`)) + assert.Assert(t, strings.Contains(stdout1.String(), `Version:"v4`)) } From cc8d1590c7db2cb38b01349785b40bb63220931a Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 21:44:28 -0700 Subject: [PATCH 3/3] test fixes Signed-off-by: Ryan Swanson --- .github/workflows/unit-tests.yaml | 4 + e2e/tests/build/build.go | 167 ++++++------ e2e/tests/render/render.go | 22 +- e2e/tests/render/testdata/helm/rendered.txt | 5 +- e2e/tests/sync/sync.go | 278 ++++++++++---------- pkg/devspace/deploy/deployer/helm/client.go | 2 +- pkg/devspace/pipeline/engine/engine_test.go | 24 +- 7 files changed, 254 insertions(+), 248 deletions(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index b6d0a4fcc1..d2c6883386 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -34,6 +34,10 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v1 + - name: Uninstall Helm 3.x + run: | + sudo rm -rf $(which helm) || true + - name: Test run: ./hack/coverage.bash diff --git a/e2e/tests/build/build.go b/e2e/tests/build/build.go index be27214d2b..1220e91cf8 100644 --- a/e2e/tests/build/build.go +++ b/e2e/tests/build/build.go @@ -9,12 +9,12 @@ import ( "os" "os/exec" "strings" - + "github.com/docker/docker/api/types/image" "github.com/onsi/ginkgo/v2" - + "github.com/docker/docker/api/types/container" - + "github.com/loft-sh/devspace/cmd" "github.com/loft-sh/devspace/cmd/flags" "github.com/loft-sh/devspace/e2e/framework" @@ -24,21 +24,21 @@ import ( ) var _ = DevSpaceDescribe("build", func() { - + initialDir, err := os.Getwd() if err != nil { panic(err) } - + // create a new factory var f factory.Factory - + // create logger var log log.Logger - + // create context ctx := context.Background() - + ginkgo.BeforeEach(func() { f = framework.NewDefaultFactory() }) @@ -47,7 +47,7 @@ var _ = DevSpaceDescribe("build", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/docker") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -58,15 +58,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + found := false Outer: for _, image := range imageList { @@ -83,7 +83,7 @@ var _ = DevSpaceDescribe("build", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/docker-skip-dependency") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -97,15 +97,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + found := false Outer: for _, image := range imageList { @@ -118,12 +118,12 @@ var _ = DevSpaceDescribe("build", func() { } framework.ExpectEqual(found, true, "image not found in cache") }) - + ginkgo.It("should build dockerfile with docker and load in kind cluster", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/docker") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -134,15 +134,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + found := false Outer: for _, image := range imageList { @@ -154,7 +154,7 @@ var _ = DevSpaceDescribe("build", func() { } } framework.ExpectEqual(found, true, "image not found in cache") - + var stdout, stderr bytes.Buffer cmd := exec.Command("kind", "load", "docker-image", "my-docker-username/helloworld:latest") cmd.Stdout = &stdout @@ -164,12 +164,12 @@ var _ = DevSpaceDescribe("build", func() { err = stderrContains(stderr.String(), "found to be already present") framework.ExpectNoError(err) }) - + ginkgo.It("should build dockerfile with docker even when KUBECONFIG is invalid", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/docker") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + _ = os.Setenv("KUBECONFIG", "i-am-invalid-config") // create build command buildCmd := &cmd.RunPipelineCmd{ @@ -181,15 +181,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + found := false Outer: for _, image := range imageList { @@ -203,7 +203,7 @@ var _ = DevSpaceDescribe("build", func() { framework.ExpectEqual(found, true, "image not found in cache") _ = os.Unsetenv("KUBECONFIG") }) - + ginkgo.It("should not build dockerfile with kaniko when KUBECONFIG is invalid", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/kaniko") framework.ExpectNoError(err) @@ -221,12 +221,12 @@ var _ = DevSpaceDescribe("build", func() { framework.ExpectError(err) _ = os.Unsetenv("KUBECONFIG") }) - + ginkgo.It("should build dockerfile with buildkit and load in kind cluster", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/buildkit") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -237,15 +237,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + for _, image := range imageList { if len(image.RepoTags) > 0 && image.RepoTags[0] == "my-docker-username/helloworld-buildkit:latest" { err = nil @@ -255,7 +255,7 @@ var _ = DevSpaceDescribe("build", func() { } } framework.ExpectNoError(err) - + var stdout, stderr bytes.Buffer cmd := exec.Command("kind", "load", "docker-image", "my-docker-username/helloworld-buildkit:latest") cmd.Stdout = &stdout @@ -265,12 +265,12 @@ var _ = DevSpaceDescribe("build", func() { err = stderrContains(stderr.String(), "found to be already present") framework.ExpectNoError(err) }) - + ginkgo.It("should build dockerfile with buildkit", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/buildkit") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -281,15 +281,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + for _, image := range imageList { if len(image.RepoTags) > 0 && image.RepoTags[0] == "my-docker-username/helloworld-buildkit:latest" { err = nil @@ -300,12 +300,12 @@ var _ = DevSpaceDescribe("build", func() { } framework.ExpectNoError(err) }) - + ginkgo.It("should build dockerfile with kaniko", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/kaniko") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -317,12 +317,12 @@ var _ = DevSpaceDescribe("build", func() { err = buildCmd.RunDefault(f) framework.ExpectNoError(err) }) - + ginkgo.It("should build dockerfile with custom builder", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/custom_build") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -333,15 +333,15 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) - + for _, image := range imageList { if len(image.RepoTags) > 0 && image.RepoTags[0] == "my-docker-username/helloworld-custom-build:latest" { err = nil @@ -352,12 +352,12 @@ var _ = DevSpaceDescribe("build", func() { } framework.ExpectNoError(err) }) - + ginkgo.It("should ignore files from Dockerfile.dockerignore only", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/dockerignore") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -368,61 +368,60 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) imageName := "my-docker-username/helloworld-dockerignore:latest" + err = errors.New("image not found") for _, image := range imageList { - if image.RepoTags[0] == imageName { + if len(image.RepoTags) > 0 && image.RepoTags[0] == imageName { err = nil break - } else { - err = errors.New("image not found") } } framework.ExpectNoError(err) - + resp, err := dockerClient.ContainerCreate(ctx, &container.Config{ Image: imageName, Cmd: []string{"/bin/ls", "./build"}, Tty: false, }, nil, nil, nil, "") framework.ExpectNoError(err) - + err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}) framework.ExpectNoError(err) - + statusCh, errCh := dockerClient.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: framework.ExpectNoError(err) case <-statusCh: } - + out, err := dockerClient.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) framework.ExpectNoError(err) - + stdout := &bytes.Buffer{} _, err = io.Copy(stdout, out) framework.ExpectNoError(err) - + err = stdoutContains(stdout.String(), "bar.txt") framework.ExpectError(err) - + err = stdoutContains(stdout.String(), "foo.txt") framework.ExpectError(err) }) - + ginkgo.It("should ignore files from Dockerfile.dockerignore relative path", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/dockerignore_rel_path") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -433,17 +432,17 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) imageName := "my-docker-username/helloworld-dockerignore-rel-path:latest" for _, image := range imageList { - if image.RepoTags[0] == imageName { + if len(image.RepoTags) > 0 && image.RepoTags[0] == imageName { err = nil break } else { @@ -451,43 +450,43 @@ var _ = DevSpaceDescribe("build", func() { } } framework.ExpectNoError(err) - + resp, err := dockerClient.ContainerCreate(ctx, &container.Config{ Image: imageName, Cmd: []string{"/bin/ls", "./build"}, Tty: false, }, nil, nil, nil, "") framework.ExpectNoError(err) - + err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}) framework.ExpectNoError(err) - + statusCh, errCh := dockerClient.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: framework.ExpectNoError(err) case <-statusCh: } - + out, err := dockerClient.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) framework.ExpectNoError(err) - + stdout := &bytes.Buffer{} _, err = io.Copy(stdout, out) framework.ExpectNoError(err) - + err = stdoutContains(stdout.String(), "bar.txt") framework.ExpectError(err) - + err = stdoutContains(stdout.String(), "foo.txt") framework.ExpectError(err) }) - + ginkgo.It("should ignore files from outside of context Dockerfile.dockerignore", func() { tempDir, err := framework.CopyToTempDir("tests/build/testdata/dockerignore_context") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + // create build command buildCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -498,17 +497,17 @@ var _ = DevSpaceDescribe("build", func() { } err = buildCmd.RunDefault(f) framework.ExpectNoError(err) - + // create devspace docker client to access docker APIs devspaceDockerClient, err := docker.NewClient(context.TODO(), log) framework.ExpectNoError(err) - + dockerClient := devspaceDockerClient.DockerAPIClient() imageList, err := dockerClient.ImageList(ctx, image.ListOptions{}) framework.ExpectNoError(err) imageName := "my-docker-username/helloworld-dockerignore-context:latest" for _, image := range imageList { - if image.RepoTags[0] == imageName { + if len(image.RepoTags) > 0 && image.RepoTags[0] == imageName { err = nil break } else { @@ -516,34 +515,34 @@ var _ = DevSpaceDescribe("build", func() { } } framework.ExpectNoError(err) - + resp, err := dockerClient.ContainerCreate(ctx, &container.Config{ Image: imageName, Cmd: []string{"/bin/ls", "./build"}, Tty: false, }, nil, nil, nil, "") framework.ExpectNoError(err) - + err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}) framework.ExpectNoError(err) - + statusCh, errCh := dockerClient.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: framework.ExpectNoError(err) case <-statusCh: } - + out, err := dockerClient.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) framework.ExpectNoError(err) - + stdout := &bytes.Buffer{} _, err = io.Copy(stdout, out) framework.ExpectNoError(err) - + err = stdoutContains(stdout.String(), "bar.txt") framework.ExpectNoError(err) - + err = stdoutContains(stdout.String(), "foo.txt") framework.ExpectError(err) }) diff --git a/e2e/tests/render/render.go b/e2e/tests/render/render.go index 8f626286f4..02b611f056 100644 --- a/e2e/tests/render/render.go +++ b/e2e/tests/render/render.go @@ -6,9 +6,9 @@ import ( "path/filepath" "strings" "sync" - + "github.com/onsi/ginkgo/v2" - + "github.com/loft-sh/devspace/cmd" "github.com/loft-sh/devspace/cmd/flags" "github.com/loft-sh/devspace/e2e/framework" @@ -16,26 +16,26 @@ import ( ) var _ = DevSpaceDescribe("build", func() { - + initialDir, err := os.Getwd() if err != nil { panic(err) } - + // create a new factory var f factory.Factory - + ginkgo.BeforeEach(func() { f = framework.NewDefaultFactory() }) - + // Test cases: - + ginkgo.It("should render helm charts", func() { tempDir, err := framework.CopyToTempDir("tests/render/testdata/helm") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + stdout := &Buffer{} // create build command renderCmd := &cmd.RunPipelineCmd{ @@ -50,18 +50,18 @@ var _ = DevSpaceDescribe("build", func() { err = renderCmd.RunDefault(f) framework.ExpectNoError(err) content := strings.TrimSpace(stdout.String()) + "\n" - + framework.ExpectLocalFileContentsImmediately( filepath.Join(tempDir, "rendered.txt"), content, ) }) - + ginkgo.It("should render kubectl deployments", func() { tempDir, err := framework.CopyToTempDir("tests/render/testdata/kubectl") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + stdout := &Buffer{} // create build command renderCmd := &cmd.RunPipelineCmd{ diff --git a/e2e/tests/render/testdata/helm/rendered.txt b/e2e/tests/render/testdata/helm/rendered.txt index 9ddbae8358..f9c5e5b2a4 100644 --- a/e2e/tests/render/testdata/helm/rendered.txt +++ b/e2e/tests/render/testdata/helm/rendered.txt @@ -9,7 +9,7 @@ metadata: "app.kubernetes.io/component": "test" "app.kubernetes.io/managed-by": "Helm" annotations: - "helm.sh/chart": "component-chart-0.9.1" + "helm.sh/chart": "component-chart-0.9.2" spec: replicas: 1 strategy: @@ -26,7 +26,7 @@ spec: "app.kubernetes.io/component": "test" "app.kubernetes.io/managed-by": "Helm" annotations: - "helm.sh/chart": "component-chart-0.9.1" + "helm.sh/chart": "component-chart-0.9.2" spec: imagePullSecrets: nodeSelector: @@ -78,7 +78,6 @@ spec: volumeMounts: initContainers: volumes: - volumeClaimTemplates: --- # Source: component-chart/templates/deployment.yaml # Create headless service for StatefulSet diff --git a/e2e/tests/sync/sync.go b/e2e/tests/sync/sync.go index 477cd74825..1ba6a586e4 100644 --- a/e2e/tests/sync/sync.go +++ b/e2e/tests/sync/sync.go @@ -6,10 +6,10 @@ import ( "path/filepath" "sync" "time" - + "github.com/onsi/ginkgo/v2" "github.com/pkg/errors" - + "github.com/loft-sh/devspace/cmd" "github.com/loft-sh/devspace/cmd/flags" "github.com/loft-sh/devspace/e2e/framework" @@ -24,32 +24,32 @@ var _ = DevSpaceDescribe("sync", func() { if err != nil { panic(err) } - + // create a new factory var ( f factory.Factory kubeClient *kube.KubeHelper ) - + ginkgo.BeforeEach(func() { f = framework.NewDefaultFactory() - + kubeClient, err = kube.NewKubeHelper() framework.ExpectNoError(err) }) - + ginkgo.It("devspace sync should override permissions on initial sync", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/permissions") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // create a new dev command deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -58,15 +58,15 @@ var _ = DevSpaceDescribe("sync", func() { }, Pipeline: "deploy", } - + // run the command err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + // wait until busybox pod is reachable _, err = kubeClient.ExecByImageSelector("busybox", ns, []string{"sh", "-c", "mkdir /test_sync && echo -n 'echo \"Hello World!\"' > /test_sync/test.sh"}) framework.ExpectNoError(err) - + // run single sync syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -78,42 +78,42 @@ var _ = DevSpaceDescribe("sync", func() { NoWatch: true, ImageSelector: "busybox", } - + // run the command err = syncCmd.Run(f) framework.ExpectNoError(err) - + // check if script is executable _, err = kubeClient.ExecByImageSelector("busybox", ns, []string{"sh", "-c", "/test_sync/test.sh"}) framework.ExpectError(err) - + // make script executable err = os.Chmod("test.sh", 0755) framework.ExpectNoError(err) - + // rerun sync command syncCmd.Ctx = nil err = syncCmd.Run(f) framework.ExpectNoError(err) - + // make sure we got the right result this time out, err := kubeClient.ExecByImageSelector("busybox", ns, []string{"sh", "-c", "/test_sync/test.sh"}) framework.ExpectNoError(err) framework.ExpectEqual(string(out), "Hello World!\n") }) - + ginkgo.It("devspace sync should work with and without config", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/no-config") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -125,11 +125,11 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + // interrupt chan for the sync command cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync with watch syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -143,53 +143,53 @@ var _ = DevSpaceDescribe("sync", func() { Wait: true, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) go func() { defer ginkgo.GinkgoRecover() defer waitGroup.Done() - + err := syncCmd.Run(f) if err != nil && errors.Cause(err) != context.Canceled { framework.ExpectNoError(err) } }() - + // wait until files were synced framework.ExpectRemoteFileContents("node:13.14-alpine", ns, "/app/file1.txt", "Hello World\n") - + // stop sync stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should execute a command after sync", func() { // TODO: // test config options dev.sync.onUpload.execRemote, dev.sync.onUpload.execRemote.onFileChange, dev.sync.onUpload.execRemote.onDirCreate, dev.sync.onUpload.execRemote.onBatch // test config options dev.sync.onDownload.execLocal, dev.sync.onDownload.execLocal.onFileChange, dev.sync.onDownload.execLocal.onDirCreate, dev.sync.onDownload.execLocal.onBatch // test config option dev.sync.onUpload.restartContainer }) - + ginkgo.It("should sync to a pod and detect changes", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/dev-simple") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // interrupt chan for the dev command cancelCtx, cancel := context.WithCancel(context.Background()) defer cancel() - + // create a new dev command devCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -199,7 +199,7 @@ var _ = DevSpaceDescribe("sync", func() { Pipeline: "dev", Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -209,48 +209,48 @@ var _ = DevSpaceDescribe("sync", func() { err = devCmd.RunDefault(f) framework.ExpectNoError(err) }() - + // wait until files were synced err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute*2, true, func(_ context.Context) (done bool, err error) { out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/app/file1.txt"}) if err != nil { return false, nil } - + return out == "Hello World", nil }) framework.ExpectNoError(err) - + // check if sub file was synced out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/app/folder1/file2.txt"}) framework.ExpectNoError(err) framework.ExpectEqual(out, "Hello World 2") - + // check if excluded file was synced _, err = kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/app/test.txt"}) framework.ExpectError(err) - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "file3.txt"), []byte(payload), 0666) framework.ExpectNoError(err) - + // wait for sync err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute*2, true, func(_ context.Context) (done bool, err error) { out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/app/file3.txt"}) if err != nil { return false, nil } - + return out == payload, nil }) framework.ExpectNoError(err) - + // check if file was downloaded through before hook _, err = os.ReadFile(filepath.Join(tempDir, "file4.txt")) framework.ExpectError(err) framework.ExpectEqual(os.IsNotExist(err), true) - + // check if file was downloaded through after hook err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, func(_ context.Context) (done bool, err error) { out, err := os.ReadFile(filepath.Join(tempDir, "file5.txt")) @@ -258,37 +258,37 @@ var _ = DevSpaceDescribe("sync", func() { if !os.IsNotExist(err) { return false, err } - + return false, nil } - + return string(out) == "Hello World", nil }) framework.ExpectNoError(err) - + // stop command cancel() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync to a pod and detect symlinked changes", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/dev-symlink") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // interrupt chan for the dev command cancelCtx, cancel := context.WithCancel(context.Background()) defer cancel() - + // create a new dev command devCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -299,7 +299,7 @@ var _ = DevSpaceDescribe("sync", func() { Pipeline: "dev", Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -309,76 +309,76 @@ var _ = DevSpaceDescribe("sync", func() { err = devCmd.RunDefault(f) framework.ExpectNoError(err) }() - + // wait until files were synced err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute*2, true, func(_ context.Context) (done bool, err error) { out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/watch/app/file1.txt"}) if err != nil { return false, nil } - + return out == "Hello World", nil }) framework.ExpectNoError(err) - + // check if sub file was synced out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/watch/app/folder1/file2.txt"}) framework.ExpectNoError(err) framework.ExpectEqual(out, "Hello World 2") - + // check if excluded file was synced _, err = kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/watch/app/ignore.txt"}) framework.ExpectError(err) - + // write a file and check that it got synced payload1 := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "/project1/app/file3.txt"), []byte(payload1), 0666) framework.ExpectNoError(err) - + err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute*2, true, func(_ context.Context) (done bool, err error) { out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/watch/app/file3.txt"}) if err != nil { return false, nil } - + return out == payload1, nil }) framework.ExpectNoError(err) - + // write a file to symlink path and check that it got synced payload2 := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "/project2/file4.txt"), []byte(payload2), 0666) framework.ExpectNoError(err) - + err = wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute*2, true, func(_ context.Context) (done bool, err error) { out, err := kubeClient.ExecByImageSelector("node", ns, []string{"cat", "/watch/app/file4.txt"}) if err != nil { return false, nil } - + return out == payload2, nil }) framework.ExpectNoError(err) - + // stop command cancel() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync to a pod and watch changes", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-simple") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -390,11 +390,11 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + // interrupt chan for the sync command cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync with watch syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -404,49 +404,49 @@ var _ = DevSpaceDescribe("sync", func() { }, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) go func() { defer ginkgo.GinkgoRecover() defer waitGroup.Done() - + err := syncCmd.Run(f) framework.ExpectNoError(err) }() - + // wait until files were synced framework.ExpectRemoteFileContents("node", ns, "/watch/file1.txt", "Hello World") - + // check if file was downloaded through after hook framework.ExpectLocalFileContents(filepath.Join(tempDir, "initial-sync-done.txt"), "Hello World") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "watching.txt"), []byte(payload), 0666) framework.ExpectNoError(err) framework.ExpectRemoteFileContents("node", ns, "/watch/watching.txt", payload) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync to a pod and not watch changes with --no-watch", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-simple") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -458,7 +458,7 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + // sync with no-watch syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -469,33 +469,33 @@ var _ = DevSpaceDescribe("sync", func() { NoWatch: true, DownloadOnInitialSync: true, } - + // start the command err = syncCmd.Run(f) framework.ExpectNoError(err) - + // wait until files were synced framework.ExpectRemoteFileContents("node", ns, "/no-watch/file1.txt", "Hello World") - + // check if file was downloaded correctly framework.ExpectLocalFileContents(filepath.Join(tempDir, "initial-sync-done-before.txt"), "Hello World") - + // check if file was downloaded through after hook framework.ExpectLocalFileNotFound(filepath.Join(tempDir, "initial-sync-done-after.txt")) }) - + ginkgo.It("should sync to a pod container with --container and --container-path", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-containers") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -507,11 +507,11 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + // sync with --container and --container-path cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ NoWarn: true, @@ -522,7 +522,7 @@ var _ = DevSpaceDescribe("sync", func() { Path: ".:/app2", Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -534,35 +534,35 @@ var _ = DevSpaceDescribe("sync", func() { framework.ExpectNoError(err) } }() - + // wait until files were synced framework.ExpectRemoteContainerFileContents("e2e=sync-containers", "container2", ns, "/app2/file1.txt", "Hello World") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "watching.txt"), []byte(payload), 0666) framework.ExpectNoError(err) framework.ExpectRemoteContainerFileContents("e2e=sync-containers", "container2", ns, "/app2/watching.txt", payload) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync to a pod container with uploadExcludePaths configuration", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-exclude-dir") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -574,10 +574,10 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync command syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -588,7 +588,7 @@ var _ = DevSpaceDescribe("sync", func() { Wait: true, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -600,38 +600,38 @@ var _ = DevSpaceDescribe("sync", func() { framework.ExpectNoError(err) } }() - + // check that uploadExcludePaths folder was not synced framework.ExpectRemoteFileNotFound("alpine", ns, "/app/node_modules") - + // check that included file was synced framework.ExpectRemoteFileContents("alpine", ns, "/app/syncme/file.txt", "I will be synced") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "watching.txt"), []byte(payload), 0666) framework.ExpectNoError(err) framework.ExpectRemoteFileContents("alpine", ns, "/app/watching.txt", payload) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync to a pod container with excludeFile, downloadExcludeFile, and uploadExcludeFile configuration", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-exclude-file") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -643,10 +643,10 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync command syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -657,7 +657,7 @@ var _ = DevSpaceDescribe("sync", func() { Wait: true, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -667,49 +667,49 @@ var _ = DevSpaceDescribe("sync", func() { err = syncCmd.Run(f) framework.ExpectNoError(err) }() - + // wait for initial sync to complete framework.ExpectLocalFileContents(filepath.Join(tempDir, "initial-sync-done.txt"), "Hello World") - + // check that included file was synced framework.ExpectRemoteFileContents("node", ns, "/app/file-include.txt", "Hello World") - + // check that excluded file was not synced framework.ExpectRemoteFileNotFound("node", ns, "/app/file-exclude.txt") - + // check that upload exluded file was not synced framework.ExpectLocalFileContents(filepath.Join(tempDir, "file-upload-exclude.txt"), "Hello World") framework.ExpectRemoteFileNotFound("node", ns, "/app/file-upload-exclude.txt") - + // check that download excluded file was not synced framework.ExpectLocalFileNotFound(filepath.Join(tempDir, "file-download-exclude.txt")) framework.ExpectRemoteFileContents("node", ns, "/app/file-download-exclude.txt", "Hello World") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "watching.txt"), []byte(payload), 0666) framework.ExpectNoError(err) framework.ExpectRemoteFileContents("node", ns, "/app/watching.txt", payload) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("should sync single file to a container", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-single-file") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -721,10 +721,10 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync command syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -735,7 +735,7 @@ var _ = DevSpaceDescribe("sync", func() { Wait: true, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -745,11 +745,11 @@ var _ = DevSpaceDescribe("sync", func() { err = syncCmd.Run(f) framework.ExpectNoError(err) }() - + // check that uploadExcludePaths folder was not synced framework.ExpectRemoteFileContents("alpine", ns, "/watch/test.txt", "Hello World") framework.ExpectRemoteFileNotFound("alpine", ns, "/watch/single.yaml") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "other-folder", "test.txt"), []byte(payload), 0666) @@ -758,7 +758,7 @@ var _ = DevSpaceDescribe("sync", func() { framework.ExpectNoError(err) framework.ExpectRemoteFileContents("alpine", ns, "/watch/test.txt", payload) framework.ExpectRemoteFileNotFound("alpine", ns, "/watch/test123.txt") - + // check that file is not created but updated _, err = kubeClient.ExecByImageSelector("alpine", ns, []string{ "sh", "-c", "echo -n 'Hello World' > /watch/test.test", @@ -770,26 +770,26 @@ var _ = DevSpaceDescribe("sync", func() { framework.ExpectNoError(err) framework.ExpectLocalFileContents(filepath.Join(tempDir, "other-folder", "test.txt"), "Hello DevSpace") framework.ExpectLocalFileNotFound(filepath.Join(tempDir, "other-folder", "test.test")) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) - + ginkgo.It("devspace sync should work with initialSync:disabled", func() { tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-initial-disabled") framework.ExpectNoError(err) defer framework.CleanupTempDir(initialDir, tempDir) - + ns, err := kubeClient.CreateNamespace("sync") framework.ExpectNoError(err) defer func() { err := kubeClient.DeleteNamespace(ns) framework.ExpectNoError(err) }() - + // deploy app to sync deployCmd := &cmd.RunPipelineCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -801,10 +801,10 @@ var _ = DevSpaceDescribe("sync", func() { } err = deployCmd.RunDefault(f) framework.ExpectNoError(err) - + cancelCtx, stop := context.WithCancel(context.Background()) defer stop() - + // sync command syncCmd := &cmd.SyncCmd{ GlobalFlags: &flags.GlobalFlags{ @@ -815,7 +815,7 @@ var _ = DevSpaceDescribe("sync", func() { Wait: true, Ctx: cancelCtx, } - + // start the command waitGroup := sync.WaitGroup{} waitGroup.Add(1) @@ -827,22 +827,22 @@ var _ = DevSpaceDescribe("sync", func() { framework.ExpectNoError(err) } }() - + // check that node_modules folder was not synced framework.ExpectRemoteFileNotFound("alpine", ns, "/app/node_modules") - + // check that included file was not synced framework.ExpectRemoteFileNotFound("alpine", ns, "/app/syncme/file.txt") - + // write a file and check that it got synced payload := randutil.GenerateRandomString(10000) err = os.WriteFile(filepath.Join(tempDir, "watching.txt"), []byte(payload), 0666) framework.ExpectNoError(err) framework.ExpectRemoteFileContents("alpine", ns, "/app/watching.txt", payload) - + // stop command stop() - + // wait for the command to finish waitGroup.Wait() }) diff --git a/pkg/devspace/deploy/deployer/helm/client.go b/pkg/devspace/deploy/deployer/helm/client.go index 6effb7c756..bc30bb2a3b 100644 --- a/pkg/devspace/deploy/deployer/helm/client.go +++ b/pkg/devspace/deploy/deployer/helm/client.go @@ -23,7 +23,7 @@ const ComponentChartFolder = "component-chart" // DevSpaceChartConfig is the config that holds the devspace chart information var DevSpaceChartConfig = &latest.ChartConfig{ Name: "component-chart", - Version: "0.9.1", + Version: "0.9.2", RepoURL: "https://charts.devspace.sh", } diff --git a/pkg/devspace/pipeline/engine/engine_test.go b/pkg/devspace/pipeline/engine/engine_test.go index 20fecc32e2..54d82080ce 100644 --- a/pkg/devspace/pipeline/engine/engine_test.go +++ b/pkg/devspace/pipeline/engine/engine_test.go @@ -3,13 +3,14 @@ package engine import ( "bytes" "context" - "mvdan.cc/sh/v3/expand" + "fmt" "os" "path/filepath" "strings" "testing" - + "gotest.tools/assert" + "mvdan.cc/sh/v3/expand" ) type testCaseShell struct { @@ -23,11 +24,11 @@ func TestShellCat(t *testing.T) { t.Fatal(err) } defer os.Remove(file.Name()) - + if _, err = file.WriteString("Hello DevSpace!"); err != nil { t.Fatalf("Unable to write to temporary file %v", err) } - + testCases := []testCaseShell{ { command: "cat " + filepath.ToSlash(file.Name()), @@ -38,7 +39,7 @@ func TestShellCat(t *testing.T) { expectedOutput: "123\n", }, } - + for _, testCase := range testCases { stdout := &bytes.Buffer{} err := ExecuteSimpleShellCommand(context.Background(), ".", expand.ListEnviron(os.Environ()...), stdout, nil, nil, testCase.command) @@ -56,7 +57,7 @@ func TestShellCatError(t *testing.T) { expectedOutput: "cat: noFile.txt: No such file or directory\n", }, } - + for _, testCase := range testCases { stderr := &bytes.Buffer{} err := ExecuteSimpleShellCommand(context.Background(), ".", expand.ListEnviron(os.Environ()...), stderr, stderr, nil, testCase.command) @@ -69,7 +70,7 @@ func TestShellCatError(t *testing.T) { } else { t.Fatal("FAIL: TestShellCatError") } - + } } @@ -80,11 +81,11 @@ func TestShellCatEnforce(t *testing.T) { t.Fatal(err) } defer os.Remove(file.Name()) - + if _, err = file.WriteString("Hello DevSpace!"); err != nil { t.Fatalf("Unable to write to temporary file %v", err) } - + testCases := []testCaseShell{ { command: "cat " + filepath.ToSlash(file.Name()), @@ -95,7 +96,7 @@ func TestShellCatEnforce(t *testing.T) { expectedOutput: "123\n", }, } - + for _, testCase := range testCases { stdout := &bytes.Buffer{} err := ExecuteSimpleShellCommand(context.Background(), ".", expand.ListEnviron(os.Environ()...), stdout, nil, nil, testCase.command) @@ -132,5 +133,8 @@ func TestHelmDownload(t *testing.T) { if err != nil { t.Fatal(err) } + + fmt.Println("👉:", stdout1.String()) + assert.Assert(t, strings.Contains(stdout1.String(), `Version:"v4`)) }