From 379e02ca48ab5fea85a936aec2bff2fcc74a3b19 Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 15:31:27 -0700 Subject: [PATCH 1/5] Update loft utils for helm v4 Signed-off-by: Ryan Swanson # Conflicts: # go.mod # go.sum # vendor/github.com/otiai10/copy/README.md # vendor/github.com/otiai10/copy/copy.go # vendor/github.com/otiai10/copy/copy_namedpipes.go # vendor/github.com/otiai10/copy/copy_namedpipes_x.go # vendor/github.com/otiai10/copy/fileinfo_go1.15.go # vendor/github.com/otiai10/copy/options.go # vendor/github.com/otiai10/copy/permission_control.go # vendor/github.com/otiai10/copy/preserve_ltimes.go # vendor/github.com/otiai10/copy/preserve_ltimes_x.go # vendor/github.com/otiai10/copy/preserve_owner.go # vendor/github.com/otiai10/copy/preserve_owner_x.go # vendor/github.com/otiai10/copy/stat_times.go # vendor/github.com/otiai10/copy/stat_times_darwin.go # vendor/github.com/otiai10/copy/stat_times_freebsd.go # vendor/github.com/otiai10/copy/stat_times_js.go # vendor/github.com/otiai10/copy/stat_times_windows.go # vendor/github.com/otiai10/copy/stat_times_x.go # vendor/github.com/otiai10/copy/test_setup.go # vendor/github.com/otiai10/copy/test_setup_x.go # vendor/modules.txt --- cmd/init.go | 248 +++++++++--------- cmd/run.go | 86 +++--- .../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 --- .../otiai10/copy/fileinfo_go1.16.go | 17 ++ 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 +- 45 files changed, 1201 insertions(+), 1153 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 create mode 100644 vendor/github.com/otiai10/copy/fileinfo_go1.16.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/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 0b29a30c81..8df4b0d283 100644 --- a/pkg/util/log/file_logger.go +++ b/pkg/util/log/file_logger.go @@ -408,3 +408,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/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/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 46a2f0a981c5ead87d96656db8f0b422d038c29a Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 17:44:03 -0700 Subject: [PATCH 2/5] 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 0630ad5216a60eac8e7ac186ef4921b99d9baf28 Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Thu, 5 Feb 2026 21:44:28 -0700 Subject: [PATCH 3/5] test fixes Signed-off-by: Ryan Swanson --- .github/workflows/unit-tests.yaml | 4 ++++ e2e/tests/render/render.go | 22 ++++++++++----------- e2e/tests/render/testdata/helm/rendered.txt | 5 ++--- pkg/devspace/deploy/deployer/helm/client.go | 2 +- pkg/devspace/pipeline/engine/engine_test.go | 3 ++- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 382b733b3e..b0606c9b54 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 shell: bash run: | 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/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..d3cb85f71b 100644 --- a/pkg/devspace/pipeline/engine/engine_test.go +++ b/pkg/devspace/pipeline/engine/engine_test.go @@ -3,13 +3,13 @@ package engine import ( "bytes" "context" - "mvdan.cc/sh/v3/expand" "os" "path/filepath" "strings" "testing" "gotest.tools/assert" + "mvdan.cc/sh/v3/expand" ) type testCaseShell struct { @@ -132,5 +132,6 @@ func TestHelmDownload(t *testing.T) { if err != nil { t.Fatal(err) } + assert.Assert(t, strings.Contains(stdout1.String(), `Version:"v4`)) } From a7355633d0bf144117dc51520add26812a276a7a Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Tue, 28 Apr 2026 17:11:24 -0600 Subject: [PATCH 4/5] - switch non-vendor downloader call sites back to the vendored logger interface - add a local helm v4 downloader command outside vendor - restore repo builds with the current vendored utils state - improve logr adapter formatting and StartWait fallback - clean up helm test/build wiring and stale loft-util references Signed-off-by: Ryan Swanson --- .github/workflows/unit-tests.yaml | 8 +- go.mod | 1 - go.sum | 2 - hack/build-all.bash | 30 ++- .../loader/variable/predefined_variable.go | 2 +- .../deploy/deployer/kubectl/kubectl.go | 105 +++++---- pkg/devspace/helm/generic/generic.go | 2 +- pkg/devspace/helm/v4/client.go | 58 ++--- .../pipeline/engine/basichandler/handler.go | 23 +- pkg/util/downloader/commands/helm_v4.go | 116 ++++++++++ pkg/util/log/logr_adapter.go | 60 ------ pkg/util/log/stream_logger.go | 6 +- pkg/util/log/stream_logger_test.go | 24 +++ ui/package-lock.json | 141 +++++++++++- .../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 +++ .../otiai10/copy/fileinfo_go1.16.go | 17 -- vendor/modules.txt | 3 - 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 +- 41 files changed, 802 insertions(+), 972 deletions(-) create mode 100644 pkg/util/downloader/commands/helm_v4.go delete mode 100644 pkg/util/log/logr_adapter.go create mode 100644 pkg/util/log/stream_logger_test.go delete mode 100644 vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go delete mode 100644 vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go create mode 100644 vendor/github.com/loft-sh/utils/pkg/log/logger.go delete mode 100644 vendor/github.com/otiai10/copy/fileinfo_go1.16.go diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index b0606c9b54..ffef514122 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -21,10 +21,10 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest] - + name: unit-test-${{ matrix.os }} runs-on: ${{ matrix.os }} - + steps: - name: Set up Go uses: actions/setup-go@v5 @@ -34,10 +34,6 @@ 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 shell: bash run: | diff --git a/go.mod b/go.mod index cec030d25a..428ba7f7a0 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,6 @@ 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 diff --git a/go.sum b/go.sum index ca69031e04..0705c5405e 100644 --- a/go.sum +++ b/go.sum @@ -343,8 +343,6 @@ 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= diff --git a/hack/build-all.bash b/hack/build-all.bash index d6741e2576..88c0bef21f 100755 --- a/hack/build-all.bash +++ b/hack/build-all.bash @@ -43,11 +43,25 @@ mkdir -p "${DEVSPACE_ROOT}/release" # Install Helm 4 echo "Installing 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 +HELM_OS=$(uname -s | tr '[:upper:]' '[:lower:]') +HELM_ARCH=$(uname -m) +case "${HELM_ARCH}" in + x86_64) + HELM_ARCH="amd64" + ;; + aarch64|arm64) + HELM_ARCH="arm64" + ;; + i386|i686) + HELM_ARCH="386" + ;; +esac +HELM_PLATFORM="${HELM_OS}-${HELM_ARCH}" +curl -s "https://get.helm.sh/helm-v4.0.4-${HELM_PLATFORM}.tar.gz" > helm4.tar.gz && tar -zxvf helm4.tar.gz "${HELM_PLATFORM}/helm" && chmod +x "${HELM_PLATFORM}/helm" # Pull the component chart COMPONENT_CHART_VERSION=$(cat pkg/devspace/deploy/deployer/helm/client.go | grep 'Version: "' | sed -nE 's/[^"]+"(.+)",\s*/\1/p') -darwin-amd64/helm pull component-chart --repo https://charts.devspace.sh --version $COMPONENT_CHART_VERSION +"${HELM_PLATFORM}/helm" pull component-chart --repo https://charts.devspace.sh --version $COMPONENT_CHART_VERSION # Move ui.tar.gz to releases echo "Moving ui" @@ -74,28 +88,28 @@ for OS in ${DEVSPACE_BUILD_PLATFORMS[@]}; do if [[ "${OS}" == "windows" ]]; then NAME="${NAME}.exe" fi - + # darwin 386 is deprecated and shouldn't be used anymore if [[ "${ARCH}" == "386" && "${OS}" == "darwin" ]]; then echo "Building for ${OS}/${ARCH} not supported." continue fi - + # arm64 build is only supported for darwin if [[ "${ARCH}" == "arm64" && "${OS}" == "windows" ]]; then echo "Building for ${OS}/${ARCH} not supported." continue fi - + echo "Building for ${OS}/${ARCH}" - + # build darwin with CGO_ENABLED=1 if [[ "${OS}" == "darwin" ]]; then CGO_ENABLED=1 else - CGO_ENABLED=0 + CGO_ENABLED=0 fi - + # build the DevSpace binary CGO_ENABLED=${CGO_ENABLED} GOARCH=${ARCH} GOOS=${OS} ${GO_BUILD_CMD} -ldflags "${GO_BUILD_LDFLAGS}"\ -o "${DEVSPACE_ROOT}/release/${NAME}" . diff --git a/pkg/devspace/config/loader/variable/predefined_variable.go b/pkg/devspace/config/loader/variable/predefined_variable.go index fca68ec318..cf7a1db372 100644 --- a/pkg/devspace/config/loader/variable/predefined_variable.go +++ b/pkg/devspace/config/loader/variable/predefined_variable.go @@ -55,7 +55,7 @@ var predefinedVars = map[string]PredefinedVariableFunction{ }, "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) + path, err := downloader.NewDownloader(commands.NewKubectlCommand(), 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 7d148d3d46..7d09239591 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,7 +17,6 @@ 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" @@ -40,7 +39,7 @@ type DeployConfig struct { IsInCluster bool InlineManifest string Manifests []string - + DeploymentConfig *latest.DeploymentConfig } @@ -51,7 +50,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 @@ -60,38 +59,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(), log.ToLogr(ctx.Log()), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx.Context()) + cmdPath, err = downloader.NewDownloader(commands.NewKubectlCommand(), 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, @@ -100,7 +99,7 @@ func New(ctx devspacecontext.Context, deployConfig *latest.DeploymentConfig) (de InlineManifest: deployConfig.Kubectl.InlineManifest, Manifests: manifests, IsInCluster: ctx.KubeClient().IsInCluster(), - + DeploymentConfig: deployConfig, }, nil } @@ -112,11 +111,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 } @@ -127,7 +126,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", @@ -139,7 +138,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 { @@ -147,41 +146,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 @@ -195,7 +194,7 @@ func (d *DeployConfig) Deploy(ctx devspacecontext.Context, _ bool) (bool, error) return false, err } } - + deployCache.Kubectl = &remotecache.KubectlCache{ Objects: kubeObjects, ManifestsHash: manifestsHash, @@ -215,29 +214,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 { @@ -249,30 +248,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 { @@ -281,21 +280,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 } @@ -304,12 +303,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 } @@ -320,11 +319,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") @@ -333,7 +332,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) } @@ -348,30 +347,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 { @@ -379,23 +378,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/generic/generic.go b/pkg/devspace/helm/generic/generic.go index f52fe8b5be..c2bca15864 100644 --- a/pkg/devspace/helm/generic/generic.go +++ b/pkg/devspace/helm/generic/generic.go @@ -34,7 +34,7 @@ func NewGenericClient(command commands.Command, logger log.Logger) Client { extract: extract.NewExtractor(), } - c.downloader = downloader.NewDownloader(command, log.ToLogr(logger), constants.DefaultHomeDevSpaceFolder) + c.downloader = downloader.NewDownloader(command, logger, constants.DefaultHomeDevSpaceFolder) return c } diff --git a/pkg/devspace/helm/v4/client.go b/pkg/devspace/helm/v4/client.go index e0e58bdd95..0dd501edf9 100644 --- a/pkg/devspace/helm/v4/client.go +++ b/pkg/devspace/helm/v4/client.go @@ -6,14 +6,14 @@ 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" "github.com/loft-sh/devspace/pkg/devspace/helm/generic" "github.com/loft-sh/devspace/pkg/devspace/helm/types" + devspacecommands "github.com/loft-sh/devspace/pkg/util/downloader/commands" "github.com/loft-sh/devspace/pkg/util/log" - "github.com/loft-sh/utils/pkg/downloader/commands" "github.com/pkg/errors" "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" @@ -26,7 +26,7 @@ type client struct { // NewClient creates a new helm v4 Client func NewClient(log log.Logger) (types.Client, error) { c := &client{} - c.genericHelm = generic.NewGenericClient(commands.NewHelmV4Command(), log) + c.genericHelm = generic.NewGenericClient(devspacecommands.NewHelmV4Command(), log) return c, nil } @@ -45,11 +45,11 @@ func (c *client) InstallChart(ctx devspacecontext.Context, releaseName string, r 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 f07af23d7b..c3c41b69cb 100644 --- a/pkg/devspace/pipeline/engine/basichandler/handler.go +++ b/pkg/devspace/pipeline/engine/basichandler/handler.go @@ -5,10 +5,11 @@ 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" + devspacecommands "github.com/loft-sh/devspace/pkg/util/downloader/commands" "github.com/loft-sh/devspace/pkg/util/log" "github.com/loft-sh/utils/pkg/downloader" "github.com/loft-sh/utils/pkg/downloader/commands" @@ -70,7 +71,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.ToLogr(log.GetFileLogger("shell")), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) + path, err := downloader.NewDownloader(commands.NewKubectlCommand(), log.GetFileLogger("shell"), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { _, _ = fmt.Fprintln(hc.Stderr, err) return "", interp.NewExitStatus(127) @@ -79,7 +80,7 @@ var EnsureCommands = map[string]func(ctx context.Context, args []string) (string }, "helm": func(ctx context.Context, args []string) (string, error) { hc := interp.HandlerCtx(ctx) - path, err := downloader.NewDownloader(commands.NewHelmV4Command(), log.ToLogr(log.GetFileLogger("shell")), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) + path, err := downloader.NewDownloader(devspacecommands.NewHelmV4Command(), log.GetFileLogger("shell"), constants.DefaultHomeDevSpaceFolder).EnsureCommand(ctx) if err != nil { _, _ = fmt.Fprintln(hc.Stderr, err) return "", interp.NewExitStatus(127) @@ -100,10 +101,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 +113,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 +121,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 +129,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 +142,7 @@ func (e *execHandler) ExecHandler(ctx context.Context, args []string) error { } } } - + return interp.DefaultExecHandler(2*time.Second)(ctx, args) } @@ -149,12 +150,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/downloader/commands/helm_v4.go b/pkg/util/downloader/commands/helm_v4.go new file mode 100644 index 0000000000..0cb7763ae7 --- /dev/null +++ b/pkg/util/downloader/commands/helm_v4.go @@ -0,0 +1,116 @@ +package commands + +import ( + "context" + "io" + "os" + "path/filepath" + "runtime" + "strings" + + utilscommand "github.com/loft-sh/utils/pkg/command" + downloadercommands "github.com/loft-sh/utils/pkg/downloader/commands" + "github.com/loft-sh/utils/pkg/extract" + "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" + "mvdan.cc/sh/v3/expand" +) + +const helmVersion = "v4.0.4" + +var helmDownload = "https://get.helm.sh/helm-" + helmVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + +func NewHelmV4Command() downloadercommands.Command { + return &helmCommand{} +} + +type helmCommand struct{} + +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 { + if runtime.GOOS == "windows" { + return helmDownload + ".zip" + } + + return helmDownload + ".tar.gz" +} + +func (h *helmCommand) IsValid(ctx context.Context, path string) (bool, error) { + out, err := utilscommand.Output(ctx, "", expand.ListEnviron(os.Environ()...), path, "version") + if err != nil { + return false, nil + } + + return strings.Contains(string(out), `:"v4.`), nil +} + +func (h *helmCommand) Install(toolHomeFolder, archiveFile string) error { + installPath, err := h.InstallPath(toolHomeFolder) + if err != nil { + return err + } + + return installHelmBinary(archiveFile, installPath, h.DownloadURL()) +} + +func installHelmBinary(archiveFile, installPath, installFromURL string) error { + targetDir := filepath.Dir(archiveFile) + extractor := extract.NewExtractor() + + if strings.HasSuffix(installFromURL, ".tar.gz") { + if err := extractor.UntarGz(archiveFile, targetDir); err != nil { + return errors.Wrap(err, "extract tar.gz") + } + } else if strings.HasSuffix(installFromURL, ".zip") { + if err := extractor.Unzip(archiveFile, targetDir); err != nil { + return errors.Wrap(err, "extract zip") + } + } + + binaryName := "helm" + if runtime.GOOS == "windows" { + binaryName = "helm.exe" + } + + sourcePath := filepath.Join(targetDir, runtime.GOOS+"-"+runtime.GOARCH, binaryName) + return copyFile(sourcePath, installPath) +} + +func copyFile(sourcePath, targetPath string) error { + source, err := os.Open(sourcePath) + if err != nil { + return err + } + defer source.Close() + + target, err := os.Create(targetPath) + if err != nil { + return err + } + defer func() { + _ = target.Close() + }() + + if _, err := io.Copy(target, source); err != nil { + return err + } + + return target.Close() +} diff --git a/pkg/util/log/logr_adapter.go b/pkg/util/log/logr_adapter.go deleted file mode 100644 index 973b66e360..0000000000 --- a/pkg/util/log/logr_adapter.go +++ /dev/null @@ -1,60 +0,0 @@ -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 0a84820cb7..99abb19319 100644 --- a/pkg/util/log/stream_logger.go +++ b/pkg/util/log/stream_logger.go @@ -468,7 +468,11 @@ func (s *StreamLogger) GetLevel() logrus.Level { } func (s *StreamLogger) StartWait(message string) { - // TODO: implement spinner/wait indicator + if message == "" { + return + } + + s.Info(message) } func (s *StreamLogger) StopWait() { diff --git a/pkg/util/log/stream_logger_test.go b/pkg/util/log/stream_logger_test.go new file mode 100644 index 0000000000..696cd0018b --- /dev/null +++ b/pkg/util/log/stream_logger_test.go @@ -0,0 +1,24 @@ +package log + +import ( + "bytes" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestStreamLoggerStartWaitFallsBackToInfo(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + logger := NewStreamLoggerWithFormat(stdout, stderr, logrus.InfoLevel, RawFormat) + + logger.StartWait("Downloading helm...") + + expected := "Downloading helm...\n" + if stdout.String() != expected { + t.Fatalf("expected %q, got %q", expected, stdout.String()) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr output, got %q", stderr.String()) + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 7af1211183..111d24079f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -128,6 +128,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2160,6 +2161,30 @@ "node": ">=0.1.95" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -2228,6 +2253,7 @@ "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "^0.2.36" }, @@ -5423,6 +5449,34 @@ "string.prototype.matchall": "^4.0.6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT", + "optional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT", + "optional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT", + "optional": true + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -5689,7 +5743,8 @@ "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/node-fetch": { "version": "2.6.13", @@ -5745,6 +5800,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.65.tgz", "integrity": "sha512-Guc3kE+W8LrQB9I3bF3blvNH15dXFIVIHIJTqrF8cp5XI/3IJcHGo4C3sJNPb8Zx49aofXKnAGIKyonE4f7XWg==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -6387,6 +6443,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6406,6 +6463,19 @@ "acorn": "^8.14.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "optional": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -6420,6 +6490,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7892,6 +7963,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.1.0.tgz", "integrity": "sha512-9EWmD0cQAbcXSc+31RIoYgEHx3KQ2CCSMDBhnXrShWvo45TMw+3/55KVxlhkG53kw9tl87DqINgHDgFVhZJV/Q==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/generator": "^7.0.0", @@ -8463,6 +8535,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -9172,6 +9245,13 @@ "node": ">=0.10.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT", + "optional": true + }, "node_modules/cross-env": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", @@ -13159,6 +13239,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/jest-circus/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -13226,6 +13323,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jest-circus/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jest-circus/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16824,6 +16943,7 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -17408,6 +17528,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19131,6 +19252,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -19574,6 +19696,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -20280,6 +20403,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -22264,6 +22388,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -23023,6 +23148,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23578,6 +23704,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT", + "optional": true + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -23659,6 +23792,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -23730,6 +23864,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -23842,6 +23977,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24112,6 +24248,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24592,6 +24729,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -25092,6 +25230,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, 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 2c120a4630..8ea86b1314 100644 --- a/vendor/github.com/loft-sh/utils/pkg/command/command.go +++ b/vendor/github.com/loft-sh/utils/pkg/command/command.go @@ -3,7 +3,6 @@ package command import ( "bytes" "context" - "errors" "fmt" "io" "os" @@ -91,8 +90,7 @@ func (s *streamCommand) RunWithEnv(ctx context.Context, dir string, environ expa err = s.cmd.Wait() } if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) && defaultStderr != nil { + if exitErr, ok := err.(*exec.ExitError); ok && defaultStderr != nil { exitErr.Stderr = defaultStderr.Bytes() } @@ -130,9 +128,8 @@ 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 { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - return fmt.Errorf("error executing '%s %s': %s", cmd, strings.Join(args, " "), string(exitErr.Stderr)) + if errr, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("error executing '%s %s': %s", cmd, strings.Join(args, " "), string(errr.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 deleted file mode 100644 index d45d9208ad..0000000000 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm.go +++ /dev/null @@ -1,91 +0,0 @@ -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 c7f800262a..2ce10dc009 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,8 +1,96 @@ 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 &helmCommand{ - version: "v3.12.3", - versionPrefix: `:"v3.`, + 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 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 deleted file mode 100644 index 6c8c249472..0000000000 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/commands/helm_v4.go +++ /dev/null @@ -1,8 +0,0 @@ -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 f1e940e042..ecb6fbc039 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" + "io/ioutil" "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 := io.ReadAll(res.Body) + content, err := ioutil.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 745c347f36..8c187bea31 100644 --- a/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go +++ b/vendor/github.com/loft-sh/utils/pkg/downloader/downloader.go @@ -2,14 +2,15 @@ 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 { @@ -19,11 +20,11 @@ type Downloader interface { type downloader struct { httpGet getRequest command commands.Command - log logr.Logger + log log.Logger toolHomeFolder string } -func NewDownloader(command commands.Command, log logr.Logger, toolHomeFolder string) Downloader { +func NewDownloader(command commands.Command, log log.Logger, toolHomeFolder string) Downloader { return &downloader{ httpGet: http.Get, command: command, @@ -64,12 +65,12 @@ func (d *downloader) downloadExecutable(command, installPath, installFromURL str err = d.downloadFile(command, installPath, installFromURL) if err != nil { - return fmt.Errorf("download file: %w", err) + return errors.Wrap(err, "download file") } err = os.Chmod(installPath, 0755) if err != nil { - return fmt.Errorf("cannot make file executable: %w", err) + return errors.Wrap(err, "cannot make file executable") } return nil @@ -78,7 +79,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", command) + d.log.Info("Downloading " + command + "...") t, err := os.MkdirTemp("", "") if err != nil { @@ -99,7 +100,7 @@ func (d *downloader) downloadFile(command, installPath, installFromURL string) e resp, err := d.httpGet(installFromURL) if err != nil { - return fmt.Errorf("get url: %w", err) + return errors.Wrap(err, "get url") } defer func(Body io.ReadCloser) { @@ -108,7 +109,7 @@ func (d *downloader) downloadFile(command, installPath, installFromURL string) e _, err = io.Copy(f, resp.Body) if err != nil { - return fmt.Errorf("download file: %w", err) + return errors.Wrap(err, "download file") } 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 32985b557e..2c7dccf057 100644 --- a/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go +++ b/vendor/github.com/loft-sh/utils/pkg/extract/unzip.go @@ -4,7 +4,6 @@ import ( "archive/tar" archivezip "archive/zip" "compress/gzip" - "errors" "fmt" "io" "os" @@ -42,7 +41,7 @@ func (e *extractor) UntarGz(src, dest string) error { tarReader := tar.NewReader(uncompressedStream) for { header, err := tarReader.Next() - if errors.Is(err, io.EOF) { + if 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 new file mode 100644 index 0000000000..39705da4cc --- /dev/null +++ b/vendor/github.com/loft-sh/utils/pkg/log/logger.go @@ -0,0 +1,32 @@ +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/fileinfo_go1.16.go b/vendor/github.com/otiai10/copy/fileinfo_go1.16.go deleted file mode 100644 index 01b3fd2499..0000000000 --- a/vendor/github.com/otiai10/copy/fileinfo_go1.16.go +++ /dev/null @@ -1,17 +0,0 @@ -//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/modules.txt b/vendor/modules.txt index 6377307506..58c43b679f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -422,9 +422,6 @@ 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 diff --git a/vendor/mvdan.cc/sh/v3/expand/arith.go b/vendor/mvdan.cc/sh/v3/expand/arith.go index 3ce199ec65..1e48a709bc 100644 --- a/vendor/mvdan.cc/sh/v3/expand/arith.go +++ b/vendor/mvdan.cc/sh/v3/expand/arith.go @@ -6,7 +6,6 @@ package expand import ( "fmt" "strconv" - "strings" "mvdan.cc/sh/v3/syntax" ) @@ -93,7 +92,7 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { if err != nil { return 0, err } - return binArit(x.Op, left, right) + return binArit(x.Op, left, right), nil default: panic(fmt.Sprintf("unexpected arithm expr: %T", x)) } @@ -106,9 +105,9 @@ func oneIf(b bool) int { return 0 } -// atoi is like strconv.Atoi, but it ignores errors and trims whitespace. +// atoi is just a shorthand for strconv.Atoi that ignores the error, +// just like shells do. func atoi(s string) int { - s = strings.TrimSpace(s) n, _ := strconv.Atoi(s) return n } @@ -130,14 +129,8 @@ 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 @@ -168,54 +161,48 @@ func intPow(a, b int) int { return p } -func binArit(op syntax.BinAritOperator, x, y int) (int, error) { +func binArit(op syntax.BinAritOperator, x, y int) int { switch op { case syntax.Add: - return x + y, nil + return x + y case syntax.Sub: - return x - y, nil + return x - y case syntax.Mul: - return x * y, nil + return x * y case syntax.Quo: - if y == 0 { - return 0, fmt.Errorf("division by zero") - } - return x / y, nil + return x / y case syntax.Rem: - if y == 0 { - return 0, fmt.Errorf("division by zero") - } - return x % y, nil + return x % y case syntax.Pow: - return intPow(x, y), nil + return intPow(x, y) case syntax.Eql: - return oneIf(x == y), nil + return oneIf(x == y) case syntax.Gtr: - return oneIf(x > y), nil + return oneIf(x > y) case syntax.Lss: - return oneIf(x < y), nil + return oneIf(x < y) case syntax.Neq: - return oneIf(x != y), nil + return oneIf(x != y) case syntax.Leq: - return oneIf(x <= y), nil + return oneIf(x <= y) case syntax.Geq: - return oneIf(x >= y), nil + return oneIf(x >= y) case syntax.And: - return x & y, nil + return x & y case syntax.Or: - return x | y, nil + return x | y case syntax.Xor: - return x ^ y, nil + return x ^ y case syntax.Shr: - return x >> uint(y), nil + return x >> uint(y) case syntax.Shl: - return x << uint(y), nil + return x << uint(y) case syntax.AndArit: - return oneIf(x != 0 && y != 0), nil + return oneIf(x != 0 && y != 0) case syntax.OrArit: - return oneIf(x != 0 || y != 0), nil + return oneIf(x != 0 || y != 0) default: // syntax.Comma // x is executed but its result discarded - return y, nil + return y } } diff --git a/vendor/mvdan.cc/sh/v3/expand/expand.go b/vendor/mvdan.cc/sh/v3/expand/expand.go index a3152dce83..2619d846fd 100644 --- a/vendor/mvdan.cc/sh/v3/expand/expand.go +++ b/vendor/mvdan.cc/sh/v3/expand/expand.go @@ -215,24 +215,6 @@ 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) @@ -332,26 +314,18 @@ 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', 'b', 'd', 'i', 'u', 'o', 'x': + case 's', 'd', 'i', 'u', 'o', 'x': arg := "" if len(args) > 0 { arg, args = args[0], args[1:] } - 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' { + var farg interface{} = arg + if c != 's' { n, _ := strconv.ParseInt(arg, 0, 0) if c == 'i' || c == 'd' { farg = int(n) @@ -361,16 +335,12 @@ 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 @@ -381,9 +351,9 @@ formatLoop: } } if len(fmts) > 0 { - return 0, fmt.Errorf("missing format char") + return "", 0, fmt.Errorf("missing format char") } - return initialArgs - len(args), nil + return buf.String(), initialArgs - len(args), nil } func (cfg *Config) fieldJoin(parts []fieldPart) string { @@ -576,22 +546,11 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { curField = nil } splitAdd := func(val string) { - 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 - } + for i, field := range strings.FieldsFunc(val, cfg.ifsRune) { + if i > 0 { flush() - } else { - if fieldStart < 0 { // starting a new field - fieldStart = i - } } - } - if fieldStart >= 0 { // ending a field without IFS - curField = append(curField, fieldPart{val: val[fieldStart:]}) + curField = append(curField, fieldPart{val: field}) } } for i, wp := range wps { @@ -677,8 +636,6 @@ 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)) } @@ -691,57 +648,31 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } // quotedElemFields returns the list of elements resulting from a quoted -// parameter expansion that should be treated especially, like "${foo[@]}". +// parameter expansion if it was in the form of ${*}, ${@}, ${foo[*], ${foo[@]}, +// or ${!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 { - switch pe.Names { - case syntax.NamesPrefixWords: // "${!prefix@}" + if pe.Names == syntax.NamesPrefixWords { 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 "@": // "${name[@]}" - switch vr := cfg.Env.Get(name); vr.Kind { - case Indexed: + case "@": + if vr := cfg.Env.Get(name); vr.Kind == 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 "*": // "${name[*]}" + case "*": if vr := cfg.Env.Get(name); vr.Kind == Indexed { return []string{cfg.ifsJoin(vr.List)} } @@ -914,22 +845,21 @@ 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, false, wantDir, newMatches) + newMatches, _ = cfg.globDir(base, dir, rxGlobStar, wantDir, newMatches) for i := len(newMatches) - 1; i >= 0; i-- { stack = append(stack, newMatches[i]) } } continue } - expr, err := pattern.Regexp(part, pattern.Filenames|pattern.EntireString) + expr, err := pattern.Regexp(part, pattern.Filenames) if err != nil { return nil, err } - rx := regexp.MustCompile(expr) - matchHidden := part[0] == byte('.') + rx := regexp.MustCompile("^" + expr + "$") var newMatches []string for _, dir := range matches { - newMatches, err = cfg.globDir(base, dir, rx, matchHidden, wantDir, newMatches) + newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) if err != nil { return nil, err } @@ -939,7 +869,7 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { return matches, nil } -func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matchHidden bool, wantDir bool, matches []string) ([]string, error) { +func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) { fullDir := dir if !filepath.IsAbs(dir) { fullDir = filepath.Join(base, dir) @@ -966,7 +896,7 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matchHidden bool // Not a symlink nor a directory. continue } - if !matchHidden && name[0] == '.' { + if !strings.HasPrefix(rx.String(), `^\.`) && 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 bf0d23820e..da77715512 100644 --- a/vendor/mvdan.cc/sh/v3/expand/param.go +++ b/vendor/mvdan.cc/sh/v3/expand/param.go @@ -160,20 +160,18 @@ 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 pe.Index != nil && vr.Kind == Indexed: + case vr.Kind == Indexed: for i, e := range vr.List { if e != "" { strs = append(strs, strconv.Itoa(i)) } } - case pe.Index != nil && vr.Kind == Associative: + case vr.Kind == Associative: for k := range vr.Map { strs = append(strs, k) } - case vr.Kind == Unset: + case !syntax.ValidName(str): 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 1ccf6fef2b..629724892e 100644 --- a/vendor/mvdan.cc/sh/v3/fileutil/file.go +++ b/vendor/mvdan.cc/sh/v3/fileutil/file.go @@ -62,7 +62,23 @@ const ( // // Deprecated: prefer CouldBeScript2, which usually requires fewer syscalls. func CouldBeScript(info os.FileInfo) ScriptConfidence { - return CouldBeScript2(fs.FileInfoToDirEntry(info)) + // 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 + } } // 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 149c936ee7..0648bd9db6 100644 --- a/vendor/mvdan.cc/sh/v3/interp/api.go +++ b/vendor/mvdan.cc/sh/v3/interp/api.go @@ -4,13 +4,6 @@ // 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 ( @@ -192,12 +185,6 @@ 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) @@ -287,7 +274,7 @@ func Params(args ...string) RunnerOption { value := fp.value() if value == "" && enable { for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i], true) + r.printOptLine(opt.name, r.opts[i]) } continue } @@ -301,7 +288,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) } @@ -379,38 +366,28 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption { } } -// optByName returns the matching runner's option index and status -func (r *Runner) optByName(name string, bash bool) (index int, status *bool) { +func (r *Runner) optByName(name string, bash bool) *bool { if bash { - for i, opt := range bashOptsTable { - if opt.name == name { - index = len(shellOptsTable) + i - return index, &r.opts[index] + for i, optName := range bashOptsTable { + if optName == name { + return &r.opts[len(shellOptsTable)+i] } } } for i, opt := range &shellOptsTable { if opt.name == name { - return i, &r.opts[i] + return &r.opts[i] } } - return 0, nil + return nil } type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool -type shellOpt struct { +var shellOptsTable = [...]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"}, @@ -422,108 +399,11 @@ var shellOptsTable = [...]shellOpt{ {' ', "pipefail"}, } -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"}, +var bashOptsTable = [...]string{ + // sorted alphabetically by name + "expand_aliases", + "globstar", + "nullglob", } // 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 2f4a43ad2e..f8161998ed 100644 --- a/vendor/mvdan.cc/sh/v3/interp/builtin.go +++ b/vendor/mvdan.cc/sh/v3/interp/builtin.go @@ -4,7 +4,6 @@ package interp import ( - "bufio" "bytes" "context" "errors" @@ -26,14 +25,12 @@ 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", "mapfile", "readarray", "shopt": + "return", "read", "shopt": return true } return false } -// TODO: oneIf and atoi are duplicated in the expand package. - func oneIf(b bool) int { if b { return 1 @@ -41,9 +38,9 @@ func oneIf(b bool) int { return 0 } -// atoi is like strconv.Atoi, but it ignores errors and trims whitespace. +// atoi is just a shorthand for strconv.Atoi that ignores the error, +// just like shells do. func atoi(s string) int { - s = strings.TrimSpace(s) n, _ := strconv.Atoi(s) return n } @@ -670,44 +667,29 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } } args := fp.args() - bash := !posixOpts if len(args) == 0 { - if bash { - for i, opt := range bashOptsTable { - r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported) + if !posixOpts { + for i, name := range bashOptsTable { + r.printOptLine(name, r.opts[len(shellOptsTable)+i]) } break } for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i], true) + r.printOptLine(opt.name, r.opts[i]) } break } for _, arg := range args { - i, opt := r.optByName(arg, bash) + opt := r.optByName(arg, !posixOpts) 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, supported) + r.printOptLine(arg, *opt) } } r.updateExpandOpts() @@ -818,64 +800,6 @@ 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)) @@ -883,37 +807,12 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a return 0 } -// 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 +func (r *Runner) printOptLine(name string, enabled bool) { + status := "off" + if enabled { + status = "on" } - r.outf("%s\t%s\t(%q not supported)\n", name, state, r.optStatusText(!enabled)) + r.outf("%s\t%s\n", name, status) } func (r *Runner) readLine(raw bool) ([]byte, error) { @@ -1088,11 +987,3 @@ 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 1bce7c8cf6..881ed83b32 100644 --- a/vendor/mvdan.cc/sh/v3/interp/handler.go +++ b/vendor/mvdan.cc/sh/v3/interp/handler.go @@ -67,10 +67,6 @@ 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 405acda5c3..13168aa958 100644 --- a/vendor/mvdan.cc/sh/v3/interp/runner.go +++ b/vendor/mvdan.cc/sh/v3/interp/runner.go @@ -6,7 +6,6 @@ package interp import ( "bytes" "context" - "errors" "fmt" "io" "math" @@ -156,19 +155,7 @@ func (r *Runner) updateExpandOpts() { func (r *Runner) expandErr(err error) { if err != nil { - 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.errf("%v\n", err) r.exitShell(context.TODO(), 1) } } @@ -311,7 +298,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 && !r.noErrExit { + } else if r.exit != 0 { r.trapCallback(ctx, r.callbackErr, "error") } if !r.keepRedirs { @@ -718,11 +705,11 @@ func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign { } func match(pat, name string) bool { - expr, err := pattern.Regexp(pat, pattern.EntireString) + expr, err := pattern.Regexp(pat, 0) if err != nil { return false } - rx := regexp.MustCompile(expr) + rx := regexp.MustCompile("(?m)^" + 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 f27db8670e..fb45699c4f 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 dbf5087eb2..08b6eafceb 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 - output io.Writer + stdout io.Writer needsPlus bool } @@ -25,7 +25,7 @@ func (r *Runner) tracer() *tracer { return &tracer{ printer: syntax.NewPrinter(), - output: r.stderr, + stdout: r.stdout, needsPlus: true, } } @@ -74,7 +74,7 @@ func (t *tracer) flush() { return } - t.output.Write(t.buf.Bytes()) + t.stdout.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 bde1ca7176..fd80f71721 100644 --- a/vendor/mvdan.cc/sh/v3/pattern/pattern.go +++ b/vendor/mvdan.cc/sh/v3/pattern/pattern.go @@ -30,10 +30,9 @@ 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}" - EntireString // match the entire string using ^$ delimiters + Shortest Mode = 1 << iota // prefer the shortest match. + Filenames // "*" and "?" don't match slashes; only "**" does + Braces // support "{a,b}" and "{1..4}" ) var numRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`) @@ -60,17 +59,11 @@ noopLoop: break noopLoop } } - if !any && mode&EntireString == 0 { // short-cut without a string copy + if !any { // 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 { @@ -79,10 +72,8 @@ 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 { @@ -91,7 +82,6 @@ writeLoop: } } else { buf.WriteString(".*") - dotMeta = true } if mode&Shortest != 0 { buf.WriteByte('?') @@ -101,7 +91,6 @@ writeLoop: buf.WriteString("[^/]") } else { buf.WriteByte('.') - dotMeta = true } case '\\': if i++; i >= len(pat) { @@ -239,13 +228,6 @@ 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 583abb61c8..133cc00d38 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.extendedGlob() { + if p.tokenizeGlob() { switch r { case '?': p.tok = globQuest @@ -359,35 +359,26 @@ skipSpace: } } -// 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 { +// tokenizeGlob determines whether the expression should be tokenized as a glob literal +func (p *Parser) tokenizeGlob() bool { if p.val == "function" { 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 + // 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 } - return false + return p.peekByte('(') } func (p *Parser) peekBytes(s string) bool { - 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) { + for p.bsp+(len(p.bs)-1) >= len(p.bs) { p.fill() } - return peekEnd <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:peekEnd], []byte(s)) + bw := p.bsp + len(s) + return bw <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:bw], []byte(s)) } func (p *Parser) peekByte(b byte) bool { @@ -512,7 +503,7 @@ func (p *Parser) regToken(r rune) token { if r = p.rune(); r == '-' { p.rune() return dashHdoc - } else if r == '<' { + } else if r == '<' && p.lang != LangPOSIX { p.rune() return wordHdoc } @@ -822,7 +813,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)-p.w]) + s = string(p.litBs[:len(p.litBs)-int(p.w)]) } p.litBs = nil return @@ -926,7 +917,7 @@ loop: tok = _Lit break loop case '?', '*', '+', '@', '!': - if p.extendedGlob() { + if p.tokenizeGlob() { tok = _Lit break loop } @@ -1077,7 +1068,7 @@ func (p *Parser) quotedHdocWord() *Word { if val == "" { return nil } - return p.wordOne(p.lit(pos, val)) + return p.word(p.wps(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 a43021f7f7..32518ec877 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/nodes.go +++ b/vendor/mvdan.cc/sh/v3/syntax/nodes.go @@ -138,9 +138,8 @@ func (p Pos) String() string { return b.String() } -// 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 ';'. +// IsValid reports whether the position is valid. All positions in nodes +// returned by Parse are valid. 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 86e7274c2c..21993b1e35 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,10 +400,12 @@ type Parser struct { accComs []Comment curComs *[]Comment - litBatch []Lit - wordBatch []wordAlloc - stmtBatch []Stmt - callBatch []callAlloc + litBatch []Lit + wordBatch []Word + wpsBatch []WordPart + stmtBatch []Stmt + stListBatch []*Stmt + callBatch []callAlloc readBuf [bufSize]byte litBuf [bufSize]byte @@ -438,10 +440,6 @@ 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 { @@ -453,12 +451,12 @@ func (p *Parser) nextPos() Pos { if !p.colOverflow { col = uint(p.col) } - return NewPos(uint(p.offs+p.bsp-p.w), line, col) + return NewPos(uint(p.offs+p.bsp-int(p.w)), line, col) } func (p *Parser) lit(pos Pos, val string) *Lit { if len(p.litBatch) == 0 { - p.litBatch = make([]Lit, 64) + p.litBatch = make([]Lit, 128) } l := &p.litBatch[0] p.litBatch = p.litBatch[1:] @@ -468,37 +466,29 @@ func (p *Parser) lit(pos Pos, val string) *Lit { return l } -type wordAlloc struct { - word Word - parts [1]WordPart -} - -func (p *Parser) wordAnyNumber() *Word { +func (p *Parser) word(parts []WordPart) *Word { if len(p.wordBatch) == 0 { - p.wordBatch = make([]wordAlloc, 32) + p.wordBatch = make([]Word, 64) } - alloc := &p.wordBatch[0] + w := &p.wordBatch[0] p.wordBatch = p.wordBatch[1:] - w := &alloc.word - w.Parts = p.wordParts(alloc.parts[:0]) + w.Parts = parts return w } -func (p *Parser) wordOne(part WordPart) *Word { - if len(p.wordBatch) == 0 { - p.wordBatch = make([]wordAlloc, 32) +func (p *Parser) wps(wp WordPart) []WordPart { + if len(p.wpsBatch) == 0 { + p.wpsBatch = make([]WordPart, 64) } - alloc := &p.wordBatch[0] - p.wordBatch = p.wordBatch[1:] - w := &alloc.word - w.Parts = alloc.parts[:1] - w.Parts[0] = part - return w + wps := p.wpsBatch[:1:1] + p.wpsBatch = p.wpsBatch[1:] + wps[0] = wp + return wps } func (p *Parser) stmt(pos Pos) *Stmt { if len(p.stmtBatch) == 0 { - p.stmtBatch = make([]Stmt, 32) + p.stmtBatch = make([]Stmt, 64) } s := &p.stmtBatch[0] p.stmtBatch = p.stmtBatch[1:] @@ -506,6 +496,15 @@ 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 @@ -574,37 +573,39 @@ func (p *Parser) postNested(s saveState) { } func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) { - buf := make([]byte, 0, 4) + var buf bytes.Buffer didUnquote := false for _, wp := range w.Parts { - buf, didUnquote = p.unquotedWordPart(buf, wp, false) + if p.unquotedWordPart(&buf, wp, false) { + didUnquote = true + } } - return buf, didUnquote + return buf.Bytes(), didUnquote } -func (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool) (_ []byte, quoted bool) { +func (p *Parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (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 = append(buf, x.Value[i]) + buf.WriteByte(x.Value[i]) } quoted = true } else { - buf = append(buf, b) + buf.WriteByte(b) } } case *SglQuoted: - buf = append(buf, []byte(x.Value)...) + buf.WriteString(x.Value) quoted = true case *DblQuoted: for _, wp2 := range x.Parts { - buf, _ = p.unquotedWordPart(buf, wp2, true) + p.unquotedWordPart(buf, wp2, true) } quoted = true } - return buf, quoted + return } func (p *Parser) doHeredocs() { @@ -644,7 +645,7 @@ func (p *Parser) doHeredocs() { // should. Look into it. l := p.lit(p.nextPos(), "") if r.Hdoc == nil { - r.Hdoc = p.wordOne(l) + r.Hdoc = p.word(p.wps(l)) } else { r.Hdoc.Parts = append(r.Hdoc.Parts, l) } @@ -921,6 +922,9 @@ 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 } @@ -964,8 +968,8 @@ func (p *Parser) invalidStmtStart() { } func (p *Parser) getWord() *Word { - if w := p.wordAnyNumber(); len(w.Parts) > 0 && p.err == nil { - return w + if parts := p.wordParts(); len(parts) > 0 && p.err == nil { + return p.word(parts) } return nil } @@ -980,18 +984,19 @@ func (p *Parser) getLit() *Lit { return nil } -func (p *Parser) wordParts(wps []WordPart) []WordPart { +func (p *Parser) wordParts() (wps []WordPart) { for { n := p.wordPart() if n == nil { - if len(wps) == 0 { - return nil // normalize empty lists into nil - } - return wps + return + } + if wps == nil { + wps = p.wps(n) + } else { + wps = append(wps, n) } - wps = append(wps, n) if p.spaced { - return wps + return } } } @@ -1004,7 +1009,7 @@ func (p *Parser) ensureNoNested() { func (p *Parser) wordPart() WordPart { switch p.tok { - case _Lit, _LitWord, _LitRedir: + case _Lit, _LitWord: l := p.lit(p.pos, p.val) p.next() return l @@ -1209,17 +1214,11 @@ func (p *Parser) wordPart() WordPart { } func (p *Parser) dblQuoted() *DblQuoted { - alloc := &struct { - quoted DblQuoted - parts [1]WordPart - }{ - quoted: DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote}, - } - q := &alloc.quoted + q := &DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote} old := p.quote p.quote = dblQuotes p.next() - q.Parts = p.wordParts(alloc.parts[:0]) + q.Parts = p.wordParts() p.quote = old q.Right = p.pos if !p.got(dblQuote) { @@ -1264,6 +1263,9 @@ 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() } @@ -1295,9 +1297,6 @@ 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() @@ -1368,9 +1367,6 @@ 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: @@ -1504,7 +1500,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.wordOne(left) + as.Value = p.word(p.wps(left)) } p.next() } else { // foo[x]=bar @@ -1647,11 +1643,6 @@ 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) } @@ -1760,7 +1751,8 @@ 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 { @@ -1771,7 +1763,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.bashFuncDecl(s) } case "declare": - if p.lang.isBash() { // Note that mksh lacks this one. + if p.lang.isBash() { p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": @@ -1783,7 +1775,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.timeClause(s) } case "coproc": - if p.lang.isBash() { // Note that mksh lacks this one. + if p.lang.isBash() { p.coprocClause(s) } case "select": @@ -1810,7 +1802,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } p.funcDecl(s, name, name.ValuePos, true) } else { - p.callExpr(s, p.wordOne(name), false) + p.callExpr(s, p.word(p.wps(name)), false) } case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: @@ -1828,7 +1820,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.callExpr(s, nil, true) break } - w := p.wordAnyNumber() + w := p.word(p.wordParts()) if p.got(leftParen) { p.posErr(w.Pos(), "invalid func name") } @@ -2112,28 +2104,18 @@ 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) - for i := len(p.accComs) - 1; i >= 0; i-- { - c := p.accComs[i] - if c.Pos().Col() != p.pos.Col() { - break + 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 } - split = i } ci.Comments = append(ci.Comments, p.accComs[:split]...) p.accComs = p.accComs[split:] - items = append(items, ci) } return @@ -2420,14 +2402,16 @@ loop: ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } - ce.Args = append(ce.Args, p.wordOne(p.lit(p.pos, p.val))) + ce.Args = append(ce.Args, p.word( + p.wps(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.wordAnyNumber()) + ce.Args = append(ce.Args, p.word(p.wordParts())) case bckQuote: if p.backquoteEnd() { break loop @@ -2436,7 +2420,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.wordAnyNumber()) + ce.Args = append(ce.Args, p.word(p.wordParts())) case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: p.doRedirect(s) @@ -2448,12 +2432,6 @@ 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 a6d6a951f8..0021c62d7c 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.wordOne(l) + x = p.word(p.wps(l)) break } pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l} pe.Index = p.eitherIndex() - x = p.wordOne(pe) + x = p.word(p.wps(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 6626b36392..7dc183a024 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/printer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/printer.go @@ -313,10 +313,8 @@ func (p *Printer) wantsNewline(pos Pos) bool { // We must have a newline here. return true } - 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. + if p.singleLine { + // The newline is optional, and singleLine turns it off. return false } // THe newline is optional, and we want it via either wantNewline or via @@ -361,7 +359,7 @@ func (p *Printer) semiOrNewl(s string, pos Pos) { if !p.minify { p.space() } - p.advanceLine(pos.Line()) + p.line = pos.Line() } p.WriteString(s) p.wantSpace = spaceRequired @@ -417,19 +415,14 @@ 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 - p.advanceLine(pos.Line()) -} - -func (p *Printer) advanceLine(line uint) { - if p.line < line { - p.line = line + if p.line < pos.Line() { + p.line++ } } @@ -493,7 +486,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.advanceLine(r.Hdoc.End().Line()) + p.line = r.Hdoc.End().Line() } p.wantSpace = spaceNotRequired } @@ -502,8 +495,6 @@ 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 @@ -512,17 +503,14 @@ func (p *Printer) newlines(pos Pos) { if !p.wantsNewline(pos) { return } - 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.newline(pos) + if pos.Line() > p.line { + if !p.minify { + // preserve single empty lines + p.WriteByte('\n') + } + p.line++ } - p.advanceLine(l) p.indent() } @@ -579,7 +567,9 @@ func (p *Printer) flushComments() { p.space() } // don't go back one line, which may happen in some edge cases - p.advanceLine(cline) + if p.line < cline { + p.line = cline + } p.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true @@ -603,14 +593,6 @@ 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) { @@ -627,7 +609,7 @@ func (p *Printer) wordParts(wps []WordPart, quoted bool) { p.line++ } p.wordPart(wp, next) - p.advanceLine(wp.End().Line()) + p.line = wp.End().Line() } } @@ -642,11 +624,11 @@ func (p *Printer) wordPart(wp, next WordPart) { p.WriteByte('\'') p.writeLit(x.Value) p.WriteByte('\'') - p.advanceLine(x.End().Line()) + p.line = x.End().Line() case *DblQuoted: p.dblQuoted(x) case *CmdSubst: - p.advanceLine(x.Pos().Line()) + p.line = x.Pos().Line() switch { case x.TempFile: p.WriteString("${") @@ -873,7 +855,7 @@ func (p *Printer) testExpr(expr TestExpr) { } func (p *Printer) testExprSameLine(expr TestExpr) { - p.advanceLine(expr.Pos().Line()) + p.line = expr.Pos().Line() switch x := expr.(type) { case *Word: p.word(x) @@ -1056,7 +1038,7 @@ func (p *Printer) stmt(s *Stmt) { } func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { - p.advanceLine(cmd.Pos().Line()) + p.line = cmd.Pos().Line() p.spacePad(cmd.Pos()) switch x := cmd.(type) { case *CallExpr: @@ -1097,22 +1079,11 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.ifClause(x, false) case *Subshell: p.WriteByte('(') - stmts := x.Stmts - if len(stmts) > 0 && startsWithLparen(stmts[0]) { + if len(x.Stmts) > 0 && startsWithLparen(x.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 @@ -1143,7 +1114,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.advanceLine(x.Y.Pos().Line()) + p.line = x.Y.Pos().Line() p.stmt(x.Y) break } @@ -1166,12 +1137,12 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { } } else { p.spacedToken(x.Op.String(), x.OpPos) - p.advanceLine(x.OpPos.Line()) + p.line = x.OpPos.Line() p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } - p.advanceLine(x.Y.Pos().Line()) + p.line = x.Y.Pos().Line() _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) p.stmt(x.Y) if indent { @@ -1192,14 +1163,13 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { } else if !x.Parens || !p.minify { p.space() } - p.advanceLine(x.Body.Pos().Line()) + p.line = 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() @@ -1239,7 +1209,6 @@ 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 } @@ -1369,10 +1338,10 @@ func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { // statement. p.comments(c) } - if p.mustNewline || !p.minify || p.wantSpace == spaceRequired { + if !p.minify || p.wantSpace == spaceRequired { p.newlines(pos) } - p.advanceLine(pos.Line()) + p.line = pos.Line() p.comments(midComs...) p.stmt(s) p.comments(endComs...) @@ -1432,8 +1401,11 @@ 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.advanceLine(a.Value.Pos().Line()) + 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.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 5a93966e25..6f245918e9 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/simplify.go +++ b/vendor/mvdan.cc/sh/v3/syntax/simplify.go @@ -10,13 +10,12 @@ 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 redundant param expansion colons ${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" func Simplify(n Node) bool { s := simplifier{} Walk(n, s.visit) @@ -38,10 +37,6 @@ 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 fd19932bf7b3f05d6396e128798022f6a86881d6 Mon Sep 17 00:00:00 2001 From: Ryan Swanson Date: Tue, 28 Apr 2026 17:39:10 -0600 Subject: [PATCH 5/5] fix: address Helm v4 review comments - avoid double-closing copied Helm binaries - return non-missing Helm validation errors - document StopWait fallback behavior - derive build Helm version from helm_v4.go - remove whitespace-only diff noise Signed-off-by: Ryan Swanson --- cmd/init.go | 246 ++++++++++++------------ cmd/run.go | 84 ++++---- e2e/tests/render/render.go | 22 +-- hack/build-all.bash | 7 +- pkg/util/downloader/commands/helm_v4.go | 18 +- pkg/util/log/stream_logger.go | 3 +- pkg/util/log/stream_logger_test.go | 1 + 7 files changed, 196 insertions(+), 185 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 60abaa0107..b9874cbd6e 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" @@ -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 41bcd47080..bbec9a5e3a 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" @@ -22,7 +22,7 @@ import ( "github.com/loft-sh/devspace/pkg/util/log" "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/e2e/tests/render/render.go b/e2e/tests/render/render.go index 02b611f056..8f626286f4 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/hack/build-all.bash b/hack/build-all.bash index 88c0bef21f..535937b6a1 100755 --- a/hack/build-all.bash +++ b/hack/build-all.bash @@ -57,7 +57,12 @@ case "${HELM_ARCH}" in ;; esac HELM_PLATFORM="${HELM_OS}-${HELM_ARCH}" -curl -s "https://get.helm.sh/helm-v4.0.4-${HELM_PLATFORM}.tar.gz" > helm4.tar.gz && tar -zxvf helm4.tar.gz "${HELM_PLATFORM}/helm" && chmod +x "${HELM_PLATFORM}/helm" +HELM_VERSION=$(sed -nE 's/^const helmVersion = "([^"]+)"/\1/p' pkg/util/downloader/commands/helm_v4.go) +if [[ -z "${HELM_VERSION}" ]]; then + echo "unable to determine Helm version" 1>&2 + exit 1 +fi +curl -s "https://get.helm.sh/helm-${HELM_VERSION}-${HELM_PLATFORM}.tar.gz" > helm4.tar.gz && tar -zxvf helm4.tar.gz "${HELM_PLATFORM}/helm" && chmod +x "${HELM_PLATFORM}/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/util/downloader/commands/helm_v4.go b/pkg/util/downloader/commands/helm_v4.go index 0cb7763ae7..1e3b952b5f 100644 --- a/pkg/util/downloader/commands/helm_v4.go +++ b/pkg/util/downloader/commands/helm_v4.go @@ -2,8 +2,10 @@ package commands import ( "context" + "errors" "io" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -12,7 +14,7 @@ import ( downloadercommands "github.com/loft-sh/utils/pkg/downloader/commands" "github.com/loft-sh/utils/pkg/extract" "github.com/mitchellh/go-homedir" - "github.com/pkg/errors" + pkgerrors "github.com/pkg/errors" "mvdan.cc/sh/v3/expand" ) @@ -55,7 +57,11 @@ func (h *helmCommand) DownloadURL() string { func (h *helmCommand) IsValid(ctx context.Context, path string) (bool, error) { out, err := utilscommand.Output(ctx, "", expand.ListEnviron(os.Environ()...), path, "version") if err != nil { - return false, nil + if errors.Is(err, exec.ErrNotFound) || os.IsNotExist(err) { + return false, nil + } + + return false, err } return strings.Contains(string(out), `:"v4.`), nil @@ -76,11 +82,11 @@ func installHelmBinary(archiveFile, installPath, installFromURL string) error { if strings.HasSuffix(installFromURL, ".tar.gz") { if err := extractor.UntarGz(archiveFile, targetDir); err != nil { - return errors.Wrap(err, "extract tar.gz") + return pkgerrors.Wrap(err, "extract tar.gz") } } else if strings.HasSuffix(installFromURL, ".zip") { if err := extractor.Unzip(archiveFile, targetDir); err != nil { - return errors.Wrap(err, "extract zip") + return pkgerrors.Wrap(err, "extract zip") } } @@ -104,11 +110,9 @@ func copyFile(sourcePath, targetPath string) error { if err != nil { return err } - defer func() { - _ = target.Close() - }() if _, err := io.Copy(target, source); err != nil { + _ = target.Close() return err } diff --git a/pkg/util/log/stream_logger.go b/pkg/util/log/stream_logger.go index 99abb19319..d3544dd2de 100644 --- a/pkg/util/log/stream_logger.go +++ b/pkg/util/log/stream_logger.go @@ -476,7 +476,8 @@ func (s *StreamLogger) StartWait(message string) { } func (s *StreamLogger) StopWait() { - // TODO: implement spinner/wait indicator + // StartWait writes a complete log line instead of starting a spinner, so + // there is no terminal state to clean up. } func (s *StreamLogger) Writer(level logrus.Level, raw bool) io.WriteCloser { diff --git a/pkg/util/log/stream_logger_test.go b/pkg/util/log/stream_logger_test.go index 696cd0018b..c9dd54ca96 100644 --- a/pkg/util/log/stream_logger_test.go +++ b/pkg/util/log/stream_logger_test.go @@ -13,6 +13,7 @@ func TestStreamLoggerStartWaitFallsBackToInfo(t *testing.T) { logger := NewStreamLoggerWithFormat(stdout, stderr, logrus.InfoLevel, RawFormat) logger.StartWait("Downloading helm...") + logger.StopWait() expected := "Downloading helm...\n" if stdout.String() != expected {