diff --git a/CHANGELOG.md b/CHANGELOG.md index d56ae6d..369dc76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # RELEASE NOTES +## 2.0.1 (Apr 29, 2025) + +### Enhancements + +* Migrated to Go `1.23.6` and adopted a semver-compliant Go directive. +* Updated the required Go version to `1.23.6` for cli-terraform compilation. +* Increased number of log messages. +* Updated vulnerable dependencies. + ## 2.0.0 (Feb 3, 2025) ### Breaking changes diff --git a/Makefile b/Makefile index 782c2cd..7528a89 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,9 @@ +GOIMPORTS_VERSION = v0.24.0 +GO_JUNIT_REPORT_VERSION = v2.1.0 +GOCOV_VERSION = v1.1.0 +GOCOVXML_VERSION = v1.1.0 +GOLANGCI_LINT_VERSION = v1.63.4 + BIN = $(CURDIR)/bin GOCMD = go GOTEST = $(GOCMD) test @@ -10,19 +16,18 @@ $(BIN)/%: | $(BIN) ; $(info $(M) Installing $(PACKAGE)...) env GOBIN=$(BIN) $(GOCMD) install $(PACKAGE) GOIMPORTS = $(BIN)/goimports -$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.24.0 +$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION) GOCOV = $(BIN)/gocov -$(BIN)/gocov: PACKAGE=github.com/axw/gocov/gocov@v1.1.0 +$(BIN)/gocov: PACKAGE=github.com/axw/gocov/gocov@$(GOCOV_VERSION) GOCOVXML = $(BIN)/gocov-xml -$(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml@v1.1.0 +$(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml@$(GOCOVXML_VERSION) GOJUNITREPORT = $(BIN)/go-junit-report -$(BIN)/go-junit-report: PACKAGE=github.com/jstemmer/go-junit-report/v2@v2.1.0 +$(BIN)/go-junit-report: PACKAGE=github.com/jstemmer/go-junit-report/v2@$(GO_JUNIT_REPORT_VERSION) GOLANGCILINT = $(BIN)/golangci-lint -GOLANGCI_LINT_VERSION = v1.63.4 $(BIN)/golangci-lint: ; $(info $(M) Installing golangci-lint...) @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(BIN) $(GOLANGCI_LINT_VERSION) diff --git a/README.md b/README.md index b936f9a..caf5ebe 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This command installs the CLI and persists the configuration and packages in `$H ### Compile from Source -**Prerequisite:** Make sure you install Go 1.22 or later. +**Prerequisite:** Make sure you install Go 1.23.6 or later. To compile Akamai CLI from source: diff --git a/go.mod b/go.mod index 228f0b3..d30db35 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/akamai/cli/v2 -go 1.22 +go 1.23.6 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -16,9 +16,9 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.19.3 - golang.org/x/net v0.34.0 - golang.org/x/sys v0.29.0 - golang.org/x/text v0.21.0 + golang.org/x/net v0.38.0 + golang.org/x/sys v0.31.0 + golang.org/x/text v0.23.0 ) require ( @@ -45,10 +45,10 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/term v0.28.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/term v0.30.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 52446be..ffa4bff 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -126,12 +126,12 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -144,19 +144,19 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/commands/command_config.go b/pkg/commands/command_config.go index f2969a6..cf9f683 100644 --- a/pkg/commands/command_config.go +++ b/pkg/commands/command_config.go @@ -35,19 +35,24 @@ func cmdConfigSet(c *cli.Context) (e error) { if e == nil { logger.Debug(fmt.Sprintf("CONFIG SET FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("CONFIG SET ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("CONFIG SET ERROR: %v", e)) } }() cfg := config.Get(c.Context) + section, key, err := parseConfigPath(c) if err != nil { - return cli.Exit(color.RedString("Unable to set config value: %s", err), 1) + logger.Error(fmt.Sprintf("Error parsing config path: %v", err)) + return cli.Exit(color.RedString("Unable to set config value: %v", err), 1) } + value := strings.Join(c.Args().Tail(), " ") cfg.SetValue(section, key, value) if err := cfg.Save(c.Context); err != nil { - return cli.Exit(color.RedString("Unable to set config value: %s", err), 1) + logger.Error(fmt.Sprintf("Error saving config: %v", err)) + return cli.Exit(color.RedString("Unable to set config value: %v", err), 1) } + return nil } @@ -60,19 +65,23 @@ func cmdConfigGet(c *cli.Context) (e error) { if e == nil { logger.Debug(fmt.Sprintf("CONFIG GET FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("CONFIG GET ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("CONFIG GET ERROR: %v", e)) } }() cfg := config.Get(c.Context) + section, key, err := parseConfigPath(c) if err != nil { - return cli.Exit(color.RedString("Unable to get config value: %s", err), 1) + logger.Error(fmt.Sprintf("Error parsing config path: %v", err)) + return cli.Exit(color.RedString("Unable to get config value: %v", err), 1) } + val, _ := cfg.GetValue(section, key) if _, err := terminal.Get(c.Context).Writeln(val); err != nil { return err } logger.Debug(val) + return nil } @@ -85,18 +94,20 @@ func cmdConfigUnset(c *cli.Context) (e error) { if e == nil { logger.Debug(fmt.Sprintf("CONFIG UNSET FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("CONFIG UNSET ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("CONFIG UNSET ERROR: %v", e)) } }() cfg := config.Get(c.Context) section, key, err := parseConfigPath(c) if err != nil { - return cli.Exit(color.RedString("Unable to unset config value: %s", err), 1) + logger.Error(fmt.Sprintf("Error parsing config path: %v", err)) + return cli.Exit(color.RedString("Unable to unset config value: %v", err), 1) } cfg.UnsetValue(section, key) if err := cfg.Save(c.Context); err != nil { - return cli.Exit(color.RedString("Unable to set config value: %s", err), 1) + logger.Error(fmt.Sprintf("Error saving config: %v", err)) + return cli.Exit(color.RedString("Unable to set config value: %v", err), 1) } return nil } @@ -110,7 +121,7 @@ func cmdConfigList(c *cli.Context) (e error) { if e == nil { logger.Debug(fmt.Sprintf("CONFIG LIST FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("CONFIG LIST ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("CONFIG LIST ERROR: %v", e)) } }() cfg := config.Get(c.Context) diff --git a/pkg/commands/command_install.go b/pkg/commands/command_install.go index 590c0e8..80f4dea 100644 --- a/pkg/commands/command_install.go +++ b/pkg/commands/command_install.go @@ -52,9 +52,9 @@ func cmdInstall(git git.Repository, langManager packages.LangManager) cli.Action } else { var exitErr cli.ExitCoder if errors.As(e, &exitErr) && exitErr.ExitCode() == 0 { - logger.Warn(fmt.Sprintf("INSTALL WARN: %v", e.Error())) + logger.Warn(fmt.Sprintf("INSTALL WARN: %v", e)) } else { - logger.Error(fmt.Sprintf("INSTALL ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("INSTALL ERROR: %v", e)) } } }() @@ -68,7 +68,7 @@ func cmdInstall(git git.Repository, langManager packages.LangManager) cli.Action repo = tools.Githubize(repo) subCmd, err := installPackage(c.Context, git, langManager, repo) if err != nil { - + logger.Error(fmt.Sprintf("Error installing package: %v", err)) return err } c.App.Commands = append(c.App.Commands, subcommandToCliCommands(*subCmd, git, langManager)...) @@ -130,8 +130,11 @@ func packageListDiff(c *cli.Context, oldcmds []subcommands) { func installPackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, repo string) (*subcommands, error) { logger := log.FromContext(ctx) + logger.Debug(fmt.Sprintf("Installing package from repository: %s", repo)) + srcPath, err := tools.GetAkamaiCliSrcPath() if err != nil { + logger.Error(fmt.Sprintf("Unable to get akamai cli source path: %v", err)) return nil, err } @@ -143,6 +146,7 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac if _, err = os.Stat(packageDir); err == nil { warningMsg := fmt.Sprintf("Package directory already exists (%s). To reinstall this package, first run 'akamai uninstall' command.", packageDir) + logger.Warn(warningMsg) return nil, cli.Exit(color.YellowString(warningMsg), 0) } @@ -153,10 +157,9 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac cmdPackage, err := readPackageFromGithub(url, dirName) if err != nil { spin.Stop(terminal.SpinnerStatusFail) - logger.Error(err.Error()) - if _, err := term.Writeln(err.Error()); err != nil { - term.WriteError(err.Error()) - } + logger.Error(fmt.Sprintf("Failed to read package from github: %v", err)) + term.WriteError(err.Error()) + if strings.Contains(err.Error(), "404") { return nil, cli.Exit(color.RedString(tools.CapitalizeFirstWord(git.ErrPackageNotAvailable.Error())), 1) } @@ -165,57 +168,63 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac spin.OK() if isBinary(cmdPackage) { - + logger.Debug(fmt.Sprintf("Installing binaries for package in directory: %s", packageDir)) ok, subCmd := installPackageBinaries(ctx, packageDir, cmdPackage, logger) if ok { return subCmd, nil } // delete package directory if err := os.RemoveAll(packageDir); err != nil { + logger.Error(fmt.Sprintf("Failed to remove package directory: %v", err)) return nil, err } - + logger.Debug(fmt.Sprintf("Unable to install binaries for package in directory: %s, cloning repository: %s", packageDir, repo)) } + spin.Start("Attempting to fetch command from %s...", repo) if !strings.HasPrefix(repo, "https://github.com/akamai/cli-") && !strings.HasPrefix(repo, "git@github.com:akamai/cli-") { term.Printf(color.CyanString(thirdPartyDisclaimer)) } + err = gitRepo.Clone(ctx, packageDir, repo, false, spin) if err != nil { + spin.Stop(terminal.SpinnerStatusFail) if err := os.RemoveAll(packageDir); err != nil { + logger.Error(fmt.Sprintf("Failed to remove package directory: %v", err)) return nil, err } - spin.Stop(terminal.SpinnerStatusFail) logger.Error(cases.Title(language.Und, cases.NoLower).String(err.Error())) return nil, cli.Exit(color.RedString(tools.CapitalizeFirstWord(err.Error())), 1) } spin.OK() + logger.Debug(fmt.Sprintf("Installing dependencies for package in directory: %s", packageDir)) + ok, subCmd := installPackageDependencies(ctx, langManager, packageDir, logger) if !ok { + logger.Error(fmt.Sprintf("Dependency installation failed, removing package directory: %s", packageDir)) if err := os.RemoveAll(packageDir); err != nil { + logger.Error(fmt.Sprintf("Failed to remove package directory: %v", err)) return nil, err } return nil, cli.Exit("Unable to install selected package", 1) } + logger.Debug(fmt.Sprintf("Dependencies installed successfully for package in directory: %s", packageDir)) return subCmd, nil } func installPackageDependencies(ctx context.Context, langManager packages.LangManager, dir string, logger *slog.Logger) (bool, *subcommands) { - cmdPackage, err := readPackage(dir) - term := terminal.Get(ctx) + term.Spinner().Start("Installing Dependencies...") - term.Spinner().Start("Installing...") + cmdPackage, err := readPackage(dir) if err != nil { term.Spinner().Stop(terminal.SpinnerStatusFail) - logger.Error(err.Error()) - if _, err := term.Writeln(err.Error()); err != nil { - term.WriteError(err.Error()) - } + logger.Error(fmt.Sprintf("Failed to read package: %v", err)) + term.WriteError(err.Error()) return false, nil } @@ -238,28 +247,31 @@ func installPackageDependencies(ctx context.Context, langManager packages.LangMa return false, nil } logger.Warn(warnMsg) + return true, &cmdPackage } if err != nil { term.Spinner().Stop(terminal.SpinnerStatusFail) + logger.Error(fmt.Sprintf("Failed to install dependecies: %v", err)) term.WriteError(err.Error()) return false, nil - } term.Spinner().OK() - return true, &cmdPackage + return true, &cmdPackage } func installPackageBinaries(ctx context.Context, dir string, cmdPackage subcommands, logger *slog.Logger) (bool, *subcommands) { - term := terminal.Get(ctx) spin := term.Spinner() spin.Start("Installing Binaries...") if err := os.MkdirAll(filepath.Join(dir, "bin"), 0700); err != nil { + spin.Stop(terminal.SpinnerStatusWarn) + logger.Error(fmt.Sprintf("Unable to create directory %s: %v", filepath.Join(dir, "bin"), err)) + term.WriteError(err.Error()) return false, nil } @@ -275,7 +287,6 @@ func installPackageBinaries(ctx context.Context, dir string, cmdPackage subcomma logger.Warn(warnMsg) return false, nil - } } @@ -283,15 +294,16 @@ func installPackageBinaries(ctx context.Context, dir string, cmdPackage subcomma if err != nil { spin.Stop(terminal.SpinnerStatusWarn) warnMsg := "Unable to save configuration file " + err.Error() + logger.Warn(warnMsg) if _, err := term.Writeln(color.YellowString(warnMsg)); err != nil { term.WriteError(err.Error()) return false, nil } - logger.Warn(warnMsg) return false, nil } spin.OK() - return true, &cmdPackage + logger.Debug(fmt.Sprintf("Binaries installed successfully in directory: %s", dir)) + return true, &cmdPackage } diff --git a/pkg/commands/command_install_test.go b/pkg/commands/command_install_test.go index 76b0ca9..f1f4f50 100644 --- a/pkg/commands/command_install_test.go +++ b/pkg/commands/command_install_test.go @@ -14,6 +14,7 @@ import ( "github.com/akamai/cli/v2/pkg/git" "github.com/akamai/cli/v2/pkg/packages" "github.com/akamai/cli/v2/pkg/terminal" + "github.com/akamai/cli/v2/pkg/tools" git2 "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -61,7 +62,7 @@ func TestCmdInstall(t *testing.T) { }) m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(nil).Once() @@ -69,13 +70,31 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() // list all packages - m.term.On("Writeln", mock.Anything).Return(0, nil) - m.term.On("Printf", mock.Anything, []interface{}(nil)).Return().Times(11) - m.term.On("Printf", mock.Anything, []interface{}{"aliases"}).Return().Twice() - m.term.On("Printf", mock.Anything, []interface{}{"alias"}).Return().Once() - m.term.On("Printf", mock.Anything, []interface{}{"commands.test help [command]"}).Return().Once() - m.term.On("Printf", mock.Anything, mock.Anything).Return().Twice() - m.term.On("Printf", mock.Anything).Return().Twice() + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) @@ -105,7 +124,7 @@ func TestCmdInstall(t *testing.T) { }) m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{"-X 'github.com/akamai/cli-test-command/cli.Version=1.0.0'"}).Return(nil).Once() @@ -113,13 +132,31 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() // list all packages - m.term.On("Writeln", mock.Anything).Return(0, nil) - m.term.On("Printf", mock.Anything, []interface{}(nil)).Return().Times(11) - m.term.On("Printf", mock.Anything, []interface{}{"aliases"}).Return().Twice() - m.term.On("Printf", mock.Anything, []interface{}{"alias"}).Return().Once() - m.term.On("Printf", mock.Anything, []interface{}{"commands.test help [command]"}).Return().Once() - m.term.On("Printf", mock.Anything, mock.Anything).Return().Twice() - m.term.On("Printf", mock.Anything).Return().Twice() + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) @@ -148,9 +185,31 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, binaryResponseStatus: http.StatusOK, teardown: func(t *testing.T) { @@ -194,7 +253,8 @@ func TestCmdInstall(t *testing.T) { mustCopyFile(t, cliJSON, cliTestCmdRepo) }) m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.gitRepo.On("Clone", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(git2.ErrRepositoryAlreadyExists) + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), + "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(git2.ErrRepositoryAlreadyExists) }, withError: "Package is not available. Supported packages can be found here: https://techdocs.akamai.com/home/page/products-tools-a-z", }, @@ -223,12 +283,9 @@ func TestCmdInstall(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - - // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) + m.term.On("WriteError", "unable to unmarshal package: invalid character 'i' looking for beginning of value").Return(0, nil).Once() }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestInvalidJSONRepo)) @@ -252,7 +309,7 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.term.On("Writeln", mock.Anything).Return(0, nil) + m.term.On("WriteError", "unable to unmarshal package: invalid character 'i' looking for beginning of value").Return(0, nil).Once() }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestInvalidJSONRepo)) @@ -284,17 +341,41 @@ func TestCmdInstall(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(packages.ErrUnknownLang).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("WarnOK").Return().Once() + m.term.On("Writeln", []interface{}{"Package installed successfully, however package type is unknown, and may or may not function correctly."}).Return(0, nil).Once() // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) @@ -331,16 +412,40 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() + m.term.On("Writeln", []interface{}{"Unable to download binary: Get \"invalid%20url/akamai/cli-test-command/releases/download/1.0.0/akamai-app-1-cmd-1\": unsupported protocol scheme \"\""}).Return(0, nil).Once() // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, binaryResponseStatus: http.StatusOK, teardown: func(t *testing.T) { @@ -378,16 +483,40 @@ func TestCmdInstall(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() + m.term.On("Writeln", []interface{}{"Unable to download binary: Get \"invalid%20url/akamai/cli-test-command/releases/download/1.0.0/akamai-app-1-cmd-1\": unsupported protocol scheme \"\""}).Return(0, nil).Once() // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) + m.term.On("Writeln", []interface{}{"\nInstalled Commands:\n"}).Return(0, nil).Once() + // first command + m.term.On("Printf", " app-1-cmd-1", []interface{}(nil)).Return().Once() + // aliases for first command + m.term.On("Printf", " (%s: ", []interface{}{"aliases"}).Return().Once() + m.term.On("Printf", "ac1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "apcmd1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ", ", []interface{}(nil)).Return().Once() + m.term.On("Printf", "test-cmd/app-1-cmd-1", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // first command description + m.term.On("Printf", " First command from app 1\n", []interface{}(nil)).Return().Once() + // second command + m.term.On("Printf", " help", []interface{}(nil)).Return().Once() + // alias for second command + m.term.On("Printf", " (%s: ", []interface{}{"alias"}).Return().Once() + m.term.On("Printf", "h", []interface{}(nil)).Return().Once() + m.term.On("Printf", ")", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + // third command + m.term.On("Printf", " install", []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}(nil)).Return(0, nil).Once() + m.term.On("Printf", "\nSee \"%s\" for details.\n", []interface{}{color.BlueString("%s help [command]", tools.Self())}).Return().Once() }, binaryResponseStatus: http.StatusNotFound, teardown: func(t *testing.T) { @@ -431,17 +560,13 @@ func TestCmdInstall(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() m.term.On("WriteError", "oops").Return(0, nil).Once() - - // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() - m.term.On("Writeln", mock.Anything).Return(0, nil) }, teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) diff --git a/pkg/commands/command_list.go b/pkg/commands/command_list.go index 339efbe..edcf174 100644 --- a/pkg/commands/command_list.go +++ b/pkg/commands/command_list.go @@ -39,7 +39,7 @@ func cmdListWithPackageReader(c *cli.Context, pr packageReader) (e error) { if e == nil { logger.Debug(fmt.Sprintf("LIST FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("LIST ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("LIST ERROR: %v", e)) } }() term := terminal.Get(c.Context) @@ -49,7 +49,8 @@ func cmdListWithPackageReader(c *cli.Context, pr packageReader) (e error) { if c.IsSet("remote") { packages, err := pr.readPackage() if err != nil { - return cli.Exit(fmt.Sprintf("list: %s", err), 1) + logger.Error(fmt.Sprintf("Failed to read package: %v", err)) + return cli.Exit(fmt.Sprintf("list: %v", err), 1) } foundCommands := true diff --git a/pkg/commands/command_search.go b/pkg/commands/command_search.go index 9bf58bf..c246db3 100644 --- a/pkg/commands/command_search.go +++ b/pkg/commands/command_search.go @@ -53,20 +53,23 @@ func cmdSearchWithPackageReader(c *cli.Context, pr packageReader) (e error) { if e == nil { logger.Debug(fmt.Sprintf("SEARCH FINISHED: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("SEARCH ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("SEARCH ERROR: %v", e)) } }() if !c.Args().Present() { + logger.Error("No keywords specified") return cli.Exit(color.RedString("You must specify one or more keywords"), 1) } packages, err := pr.readPackage() if err != nil { + logger.Error(fmt.Sprintf("Failed to read package: %v", err)) return cli.Exit(color.RedString(err.Error()), 1) } err = searchPackages(c.Context, c.Args().Slice(), packages) if err != nil { + logger.Error(fmt.Sprintf("Failed to search packages: %v", err)) return cli.Exit(color.RedString(err.Error()), 1) } @@ -199,7 +202,7 @@ func getLatestVersion(s string) (string, error) { u, err := url.Parse(s) if err != nil { - return "", fmt.Errorf("error parsing URL: %s", err.Error()) + return "", fmt.Errorf("error parsing URL: %v", err) } // extract the last string of the package URL @@ -208,7 +211,7 @@ func getLatestVersion(s string) (string, error) { repoURL := fmt.Sprintf(githubURLTemplate, lastSegment) resp, err := http.Get(repoURL) if err != nil { - return "", fmt.Errorf("error fetching the URL: %s", err.Error()) + return "", fmt.Errorf("error fetching the URL: %v", err) } defer func() { if err := resp.Body.Close(); err != nil { @@ -264,13 +267,13 @@ func getVersionFromSystem(command string) (string, error) { } body, err := os.ReadFile(filepath.Join(finalPath, "cli.json")) if err != nil { - return "", fmt.Errorf("Error reading the file: %s", err.Error()) + return "", fmt.Errorf("error reading the file: %v", err) } var cli CLI if err := json.Unmarshal(body, &cli); err != nil { - return "", fmt.Errorf("Error parsing the JSON: %s", err.Error()) + return "", fmt.Errorf("error parsing the JSON: %v", err) } return cli.CommandList[0].Version, nil diff --git a/pkg/commands/command_subcommand.go b/pkg/commands/command_subcommand.go index a19a5b8..93e797f 100644 --- a/pkg/commands/command_subcommand.go +++ b/pkg/commands/command_subcommand.go @@ -30,11 +30,24 @@ import ( ) func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.ActionFunc { - return func(c *cli.Context) error { + return func(c *cli.Context) (e error) { c.Context = log.WithCommandContext(c.Context, c.Command.Name) logger := log.FromContext(c.Context) term := terminal.Get(c.Context) + defer func() { + if e != nil { + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Command execution failed: %v", e)) + } else { + term.Spinner().OK() + logger.Info("Command execution completed") + } + }() + + logger.Info(fmt.Sprintf("Executing subcommand: %s", c.Command.Name)) + term.Spinner().Start(fmt.Sprintf("Running %s command...", c.Command.Name)) + commandName := strings.ToLower(c.Command.Name) executable, _, err := findExec(c.Context, langManager, commandName) @@ -51,11 +64,16 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act packageDir = findPackageDir(executable[1]) } - cmdPackage, _ := readPackage(packageDir) + cmdPackage, err := readPackage(packageDir) + if err != nil { + logger.Error(fmt.Sprintf("Error reading package: %v", err)) + return err + } if cmdPackage.Requirements.Python != "" { exec, err := langManager.FindExec(c.Context, cmdPackage.Requirements, packageDir) if err != nil { + logger.Error(fmt.Sprintf("Error finding executable: %v", err)) return err } @@ -80,6 +98,7 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act answer, err := term.Confirm("Would you like to reinstall it", true) logger.Debug(fmt.Sprintf("Would you like to reinstall it? %v", answer)) if err != nil { + logger.Error(fmt.Sprintf("Error confirming reinstall: %v", err)) return err } if !answer { @@ -96,6 +115,7 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act } } if err := os.Setenv("PYTHONUSERBASE", packageDir); err != nil { + logger.Error(fmt.Sprintf("Error setting PYTHONUSERBASE: %v", err)) return err } } @@ -115,14 +135,17 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act } if err := os.Setenv("AKAMAI_CLI_COMMAND", commandName); err != nil { + logger.Error(fmt.Sprintf("Error setting AKAMAI_CLI_COMMAND: %v", err)) return err } if err := os.Setenv("AKAMAI_CLI_COMMAND_VERSION", currentCmd.Version); err != nil { + logger.Error(fmt.Sprintf("Error setting AKAMAI_CLI_COMMAND_VERSION: %v", err)) return err } cmdPackage, err = readPackage(packageDir) if err != nil { + logger.Error(fmt.Sprintf("Error reading package: %v", err)) return err } diff --git a/pkg/commands/command_subcommand_test.go b/pkg/commands/command_subcommand_test.go index 55472b5..5f4528a 100644 --- a/pkg/commands/command_subcommand_test.go +++ b/pkg/commands/command_subcommand_test.go @@ -31,18 +31,30 @@ func TestCmdSubcommand(t *testing.T) { command: "echo", args: []string{"abc"}, init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running echo command...", []interface{}(nil)).Return().Once() + m.langManager.On("PrepareExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, akamaiEchoBin).Return([]string{akamaiEchoBin}, nil) + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "run installed akamai echo command as binary with edgerc location": { command: "echo", args: []string{"abc"}, init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running echo command...", []interface{}(nil)).Return().Once() + m.langManager.On("PrepareExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, akamaiEchoBin).Return([]string{akamaiEchoBin}, nil) + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "run installed akamai echo command as binary with alias": { @@ -51,35 +63,58 @@ func TestCmdSubcommand(t *testing.T) { edgercLocation: "some/location", section: "some_section", init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running e command...", []interface{}(nil)).Return().Once() + m.langManager.On("PrepareExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, akamaiEBin).Return([]string{akamaiEBin}, nil) + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "run installed akamai echo command as .cmd file": { command: "echo-cmd", args: []string{"abc"}, init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running echo-cmd command...", []interface{}(nil)).Return().Once() + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, filepath.Join("testdata", ".akamai-cli", "src", "cli-echo", "bin", "akamai-echo-cmd.cmd")). Return([]string{filepath.Join("testdata", ".akamai-cli", "src", "cli-echo", "bin", "akamai-echo-cmd.cmd")}, nil) m.langManager.On("PrepareExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "run installed python akamai echo command": { command: "echo-cmd", args: []string{"abc"}, init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running echo-cmd command...", []interface{}(nil)).Return().Once() + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, filepath.Join("testdata", ".akamai-cli", "src", "cli-echo", "bin", "akamai-echo-cmd.cmd")). Return([]string{filepath.Join("testdata", ".akamai-cli", "src", "cli-echo", "bin", "akamai-echo-cmd.cmd")}, nil) m.langManager.On("PrepareExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Go: "1.14.0"}, "cli-echo").Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "executable not found": { - command: "invalid", - args: []string{"abc"}, - init: func(_ *mocked) {}, + command: "invalid", + args: []string{"abc"}, + init: func(m *mocked) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running invalid command...", []interface{}(nil)).Return().Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Fail").Return().Once() + }, withError: `Executable "invalid" not found`, }, } @@ -138,6 +173,8 @@ func TestPythonCmdSubcommand(t *testing.T) { // If python is not available, just skip the test t.Skipf("We could not find any available Python binary, thus we skip this test. Details: \n%s", err.Error()) } else { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Running echo-python command...", []interface{}(nil)).Return().Once() m.langManager.On("PrepareExecution", packages.LanguageRequirements{Python: "3.0.0"}, "cli-echo-python").Return(nil).Once() m.langManager.On("FinishExecution", packages.LanguageRequirements{Python: "3.0.0"}, "cli-echo-python").Return(nil).Once() m.langManager.On("FindExec", packages.LanguageRequirements{Python: "3.0.0"}, filepath.Join("testdata", ".akamai-cli", "src", "cli-echo-python", "bin", "akamai-echo-python")). @@ -145,6 +182,8 @@ func TestPythonCmdSubcommand(t *testing.T) { m.langManager.On("FindExec", packages.LanguageRequirements{Python: "3.0.0"}, filepath.Join("testdata", ".akamai-cli", "src", "cli-echo-python")). Return([]string{pythonBin, filepath.Join("testdata", ".akamai-cli", "src", "cli-echo-python", "bin", "akamai-echo-python")}, nil).Once() m.langManager.On("FileExists", filepath.Join("testdata", ".akamai-cli", "venv", "cli-echo-python")).Return(true, nil) + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() } err := app.RunContext(ctx, args) diff --git a/pkg/commands/command_uninstall.go b/pkg/commands/command_uninstall.go index 5afbd94..c2a45f8 100644 --- a/pkg/commands/command_uninstall.go +++ b/pkg/commands/command_uninstall.go @@ -43,12 +43,12 @@ func cmdUninstall(langManager packages.LangManager) cli.ActionFunc { if e == nil { logger.Debug(fmt.Sprintf("UNINSTALL FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("UNINSTALL ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("UNINSTALL ERROR: %v", e)) } }() for _, cmd := range c.Args().Slice() { if err := uninstallPackage(c.Context, langManager, cmd, logger); err != nil { - logger.Error(err.Error()) + logger.Error(fmt.Sprintf("Error uninstalling package: %v", err)) return cli.Exit(color.RedString(err.Error()), 1) } } @@ -62,28 +62,33 @@ func uninstallPackage(ctx context.Context, langManager packages.LangManager, cmd home, err := homedir.Dir() if err != nil { - return fmt.Errorf("no home directory detected: %s", err) + logger.Error(fmt.Sprintf("No home directory detected: %v", err)) + return fmt.Errorf("no home directory detected: %v", err) } home += string(filepath.Separator) exec, _, err := findExec(ctx, langManager, cmd) if err != nil { if !errors.Is(err, packages.ErrNoExeFound) { - return fmt.Errorf("command \"%s\" not found. Try \"%s help\" : %s", cmd, tools.Self(), err) + logger.Error(fmt.Sprintf("Command \"%s\" not found: %v", cmd, err)) + return fmt.Errorf("command \"%s\" not found. Try \"%s help\" : %v", cmd, tools.Self(), err) } + // err = ErrNoExeFound - there is a directory but without any executables paths := filepath.SplitList(getPackageBinPaths()) for i, path := range paths { - // trim home directory part of a path to exclude cases where command name could be a part of it path = strings.TrimPrefix(path, home) // if trimmed path (akamai-cli defined) contains name of command to uninstall, delete directory if strings.Contains(path, cmd) { if err = os.RemoveAll(paths[i]); err != nil { - return fmt.Errorf("could not remove directory %s: %s", paths[i], err) + logger.Error(fmt.Sprintf("Unable to remove directory: %s", paths[i])) + return fmt.Errorf("could not remove directory %s: %v", paths[i], err) } + logger.Debug(fmt.Sprintf("Removed directory: %s", paths[i])) return nil } } + logger.Error(fmt.Sprintf("Command \"%s\" not found", cmd)) return fmt.Errorf("command \"%s\" not found. Try \"%s help\"", cmd, tools.Self()) } @@ -99,30 +104,33 @@ func uninstallPackage(ctx context.Context, langManager packages.LangManager, cmd if repoDir == "" { term.Spinner().Fail() - logger.Error("unable to uninstall, was it installed using \"akamai install\"?") + logger.Error("Unable to uninstall, was it installed using \"akamai install\"?") return errors.New("unable to uninstall, was it installed using " + color.CyanString("\"akamai install\"") + "?") } if err := os.RemoveAll(repoDir); err != nil { term.Spinner().Fail() - logger.Error(fmt.Sprintf("unable to remove directory: %s", repoDir)) - return fmt.Errorf("unable to remove directory: %s", repoDir) + logger.Error(fmt.Sprintf("Unable to remove directory: %s", repoDir)) + return fmt.Errorf("unable to remove directory %s: %v", repoDir, err) } venvPath, err := tools.GetPkgVenvPath(fmt.Sprintf("cli-%s", cmd)) if err != nil { + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Unable to get virtualenv path: %v", err)) return err } if _, err := os.Stat(venvPath); err == nil || !os.IsNotExist(err) { logger.Debug("Attempting to remove package virtualenv directory") if err := os.RemoveAll(venvPath); err != nil { term.Spinner().Fail() - logger.Error(fmt.Sprintf("unable to remove virtualenv directory: %s", venvPath)) - return fmt.Errorf("unable to remove virtualenv directory: %s", repoDir) + logger.Error(fmt.Sprintf("Unable to remove virtualenv directory: %s", venvPath)) + return fmt.Errorf("unable to remove virtualenv directory %s: %v", repoDir, err) } } term.Spinner().OK() + logger.Debug(fmt.Sprintf("Uninstalled \"%s\" command", cmd)) return nil } diff --git a/pkg/commands/command_update.go b/pkg/commands/command_update.go index 0f2507b..9f6ebd3 100644 --- a/pkg/commands/command_update.go +++ b/pkg/commands/command_update.go @@ -45,7 +45,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act if e == nil { logger.Debug(fmt.Sprintf("UPDATE FINISH: %v", time.Since(start))) } else { - logger.Error(fmt.Sprintf("UPDATE ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("UPDATE ERROR: %v", e)) } }() if !c.Args().Present() { @@ -58,6 +58,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act for _, command := range cmd.Commands { if _, ok := builtinCmds[command.Name]; !ok { if err := updatePackage(c.Context, gitRepo, langManager, logger, command.Name); err != nil { + logger.Error(fmt.Sprintf("Error updating package: %v", err)) return err } } @@ -69,6 +70,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act for _, cmd := range c.Args().Slice() { if err := updatePackage(c.Context, gitRepo, langManager, logger, cmd); err != nil { + logger.Error(fmt.Sprintf("Error updating package: %v", err)) return err } } @@ -81,6 +83,7 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack term := terminal.Get(ctx) exec, _, err := findExec(ctx, langManager, cmd) if err != nil { + logger.Error(fmt.Sprintf("Command \"%s\" not found: %v", cmd, err)) return cli.Exit(color.RedString("Command \"%s\" not found. Try \"%s help\".\n", cmd, tools.Self()), 1) } @@ -98,6 +101,7 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack if repoDir == "" { term.Spinner().Fail() + logger.Error("Unable to find package directory") return cli.Exit(color.RedString("unable to update, was it installed using "+color.CyanString("\"akamai install\"")+"?"), 1) } @@ -105,12 +109,13 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack err = gitRepo.Open(repoDir) if err != nil { - logger.Debug("Unable to open repo") cmdPackage, err := readPackage(repoDir) if err != nil { - return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Failed to read package: %v", err)) + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %v", err), 1) } packageVersions := map[string]string{} @@ -123,7 +128,9 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack remotePackage, err := readPackageFromGithub(url, repoDir) if err != nil { - return cli.Exit(color.RedString("unable to update, there was an issue with fetching latest configuration file: %s", err.Error()), 1) + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Failed to read package from github: %v", err)) + return cli.Exit(color.RedString("unable to update, there was an issue with fetching latest configuration file: %v", err), 1) } remoteVersions := map[string]string{} @@ -136,6 +143,7 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack debugMessage := fmt.Sprintf("command \"%s\" already up-to-date", cmd) logger.Warn(debugMessage) if _, err := term.Writeln(color.CyanString(debugMessage)); err != nil { + term.WriteError(err.Error()) return err } return nil @@ -144,37 +152,48 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack tempDir := filepath.Dir(repoDir) + "/.tmp_" + filepath.Base(repoDir) logger.Debug(fmt.Sprintf("Moving package to temporary dir: %s", tempDir)) if err = os.Rename(repoDir, tempDir); err != nil { - return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Unable to move package to temporary dir: %v", err)) + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %v", err), 1) } + logger.Debug(fmt.Sprintf("Attempting to install package: %s", cmd)) _, err = installPackage(ctx, gitRepo, langManager, tools.Githubize(cmd)) if err != nil { term.Spinner().Fail() if err := os.Rename(tempDir, repoDir); err != nil { - return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + logger.Error(fmt.Sprintf("Unable to move package back to original dir: %v", err)) + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %v", err), 1) } - return cli.Exit(color.RedString("unable to update: %s", err.Error()), 1) + logger.Error(fmt.Sprintf("Failed to install package: %v", err)) + return cli.Exit(color.RedString("unable to update: %v", err), 1) } if err := os.RemoveAll(tempDir); err != nil { - return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + term.Spinner().Fail() + logger.Error(fmt.Sprintf("Unable to remove temporary dir: %v", err)) + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %v", err), 1) } - logger.Debug("Repo updated successfully") term.Spinner().OK() - return nil + logger.Debug("Repo updated successfully") + return nil } err = updateRepo(ctx, gitRepo, logger, term, cmd) if err != nil { + logger.Error(fmt.Sprintf("Failed to update repo: %v", err)) return err } if ok, _ := installPackageDependencies(ctx, langManager, repoDir, logger); !ok { + term.Spinner().Fail() logger.Debug("Error updating dependencies") return cli.Exit("Unable to update command", 1) } + + term.Spinner().OK() logger.Debug("Repo updated successfully") return nil @@ -183,15 +202,15 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack func updateRepo(ctx context.Context, gitRepo git.Repository, logger *slog.Logger, term terminal.Terminal, cmd string) error { w, err := gitRepo.Worktree() if err != nil { - logger.Debug("Unable to open repo") term.Spinner().Fail() - return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + logger.Error("Unable to open repo") + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %v", err), 1) } if err := gitRepo.Reset(&gogit.ResetOptions{Mode: gogit.HardReset}); err != nil { - logger.Debug(err.Error()) term.Spinner().Warn() - if _, err := term.Writeln(color.YellowString("unable to reset the branch changes, we will try to continue anyway: %s", err.Error())); err != nil { + logger.Error(fmt.Sprintf("Unable to reset the branch changes: %v", err)) + if _, err := term.Writeln(color.YellowString("unable to reset the branch changes, we will try to continue anyway: %v", err)); err != nil { return err } } @@ -203,23 +222,23 @@ func updateRepo(ctx context.Context, gitRepo git.Repository, logger *slog.Logger logger.Debug(fmt.Sprintf("Using ref: %s", refName)) if errBeforePull != nil { - logger.Debug(fmt.Sprintf("Fetch error: %s", errBeforePull.Error())) term.Spinner().Fail() - return cli.Exit(color.RedString("Unable to fetch updates (%s)", errBeforePull.Error()), 1) + logger.Error(fmt.Sprintf("Fetch error: %v", errBeforePull)) + return cli.Exit(color.RedString("Unable to fetch updates: %v", errBeforePull), 1) } err = gitRepo.Pull(ctx, w) if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) { - logger.Debug(tools.CapitalizeFirstWord(err.Error())) term.Spinner().Fail() + logger.Error(fmt.Sprintf("Fetch error: %v", err)) return cli.Exit(color.RedString(tools.CapitalizeFirstWord(err.Error())), 1) } ref, err := gitRepo.Head() if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) { - logger.Debug(fmt.Sprintf("Fetch error: %s", err.Error())) term.Spinner().Fail() - return cli.Exit(color.RedString("Unable to fetch updates (%s)", err.Error()), 1) + logger.Error(fmt.Sprintf("Fetch error: %v", err)) + return cli.Exit(color.RedString("Unable to fetch updates: %v", err), 1) } if refBeforePull.Hash() != ref.Hash() { @@ -228,19 +247,18 @@ func updateRepo(ctx context.Context, gitRepo git.Repository, logger *slog.Logger logger.Debug(fmt.Sprintf("Latest commit: %s", commit)) if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) { - logger.Debug(fmt.Sprintf("Fetch error: %s", err.Error())) term.Spinner().Fail() - return cli.Exit(color.RedString("Unable to fetch updates (%s)", err.Error()), 1) + logger.Error(fmt.Sprintf("Fetch error: %v", err)) + return cli.Exit(color.RedString("Unable to fetch updates: %v", err), 1) } } else { logger.Debug(fmt.Sprintf("HEAD is the same as the remote: %s (old) vs %s (new)", refBeforePull.Hash().String(), ref.Hash().String())) - term.Spinner().WarnOK() debugMessage := fmt.Sprintf("command \"%s\" already up-to-date", cmd) logger.Warn(debugMessage) if _, err := term.Writeln(color.CyanString(debugMessage)); err != nil { return err } } - term.Spinner().OK() + return nil } diff --git a/pkg/commands/command_update_test.go b/pkg/commands/command_update_test.go index 39e41b3..850f88e 100644 --- a/pkg/commands/command_update_test.go +++ b/pkg/commands/command_update_test.go @@ -53,7 +53,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() @@ -80,7 +80,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() @@ -110,7 +110,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() // installing update - m.term.On("Start", `Installing...`, []interface{}(nil)).Return().Once() + m.term.On("Start", `Installing Dependencies...`, []interface{}(nil)).Return().Once() m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() @@ -143,7 +143,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() @@ -171,16 +171,16 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.term.On("Writeln", mock.Anything).Return(0, nil).Once() m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(fmt.Errorf("oops")).Once() - m.term.On("WriteError", "oops") + m.term.On("WriteError", "oops").Return(0, nil).Once() - m.term.On("OK").Return().Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Fail").Return().Once() }, withError: "Unable to update command", }, @@ -205,7 +205,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() }, - withError: "Unable to fetch updates (oops)", + withError: "Unable to fetch updates: oops", }, "error getting HEAD of repository after pull": { args: []string{"echo"}, @@ -226,7 +226,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() }, - withError: "Unable to fetch updates (oops)", + withError: "Unable to fetch updates: oops", }, "error pulling repository": { args: []string{"echo"}, @@ -265,7 +265,7 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() }, - withError: "Unable to fetch updates (oops)", + withError: "Unable to fetch updates: oops", }, "error getting worktree": { args: []string{"echo"}, @@ -305,6 +305,8 @@ func TestCmdUpdate(t *testing.T) { m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("WarnOK").Return().Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() }, }, "error opening repository, update from remote, success": { @@ -344,7 +346,7 @@ func TestCmdUpdate(t *testing.T) { }) m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() @@ -397,10 +399,10 @@ func TestCmdUpdate(t *testing.T) { m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(fmt.Errorf("oops")).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Installing Dependencies...", []interface{}(nil)).Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() - m.term.On("WriteError", "oops") + m.term.On("WriteError", "oops").Return(0, nil).Once() }, teardown: func(t *testing.T) { diff --git a/pkg/commands/command_upgrade_test.go b/pkg/commands/command_upgrade_test.go index fc5ea24..ccba14b 100644 --- a/pkg/commands/command_upgrade_test.go +++ b/pkg/commands/command_upgrade_test.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" "testing" + "time" "github.com/akamai/cli/v2/pkg/config" "github.com/akamai/cli/v2/pkg/terminal" @@ -25,7 +26,7 @@ func mockIsTTY(term *terminal.Mock, isTTY bool) { func mockGetAndUpdateLastUpgradeCheck(cfg *config.Mock, lastUpgradeCheckValue string) { cfg.On("GetValue", "cli", "last-upgrade-check").Return(lastUpgradeCheckValue, true).Once() - cfg.On("SetValue", "cli", "last-upgrade-check", mock.AnythingOfType("string")).Return().Once() + cfg.On("SetValue", "cli", "last-upgrade-check", time.Now().Format(time.RFC3339)).Return().Once() cfg.On("Save").Return(nil).Once() } @@ -251,8 +252,10 @@ func (m *mockVersionProvider) getCurrentVersion() string { return args.String(0) } -func (m *mockVersionProvider) mockVersions(latestVersion, currentVersion string) { - m.On("getLatestReleaseVersion", mock.Anything).Return(latestVersion).Once() +func (m *mockVersionProvider) mockVersions(term *terminal.Mock, cfg *config.Mock, latestVersion, currentVersion string) { + ctx := terminal.Context(context.Background(), term) + ctx = config.Context(ctx, cfg) + m.On("getLatestReleaseVersion", ctx).Return(latestVersion).Once() m.On("getCurrentVersion").Return(currentVersion).Once() } @@ -275,7 +278,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "never") - vp.mockVersions("2.0.0", "1.2.3") + vp.mockVersions(term, cfg, "2.0.0", "1.2.3") mockStopSpinner(term) mockConfirmUpgrade(term, "2.0.0", "1.2.3", true) }, @@ -286,7 +289,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "never") - vp.mockVersions("2.0.0", "1.2.3") + vp.mockVersions(term, cfg, "2.0.0", "1.2.3") mockStopSpinner(term) mockConfirmUpgrade(term, "2.0.0", "1.2.3", false) }, @@ -297,7 +300,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "never") - vp.mockVersions("2.0.0", "2.0.0") + vp.mockVersions(term, cfg, "2.0.0", "2.0.0") }, expectedResult: "2.0.0", }, @@ -306,7 +309,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "never") - vp.mockVersions("1.0.0", "1.2.3") + vp.mockVersions(term, cfg, "1.0.0", "1.2.3") }, expectedResult: "", }, @@ -315,7 +318,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "never") - vp.mockVersions("1.0.0", "1.2.3") + vp.mockVersions(term, cfg, "1.0.0", "1.2.3") }, expectedResult: "", }, @@ -324,7 +327,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "2024-12-31T11:55:26+01:00") - vp.mockVersions("2.0.0", "1.2.3") + vp.mockVersions(term, cfg, "2.0.0", "1.2.3") mockStopSpinner(term) mockConfirmUpgrade(term, "2.0.0", "1.2.3", true) }, @@ -349,7 +352,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "ignore") - vp.mockVersions("2.0.0", "1.2.3") + vp.mockVersions(term, cfg, "2.0.0", "1.2.3") mockStopSpinner(term) mockConfirmUpgrade(term, "2.0.0", "1.2.3", true) }, @@ -360,7 +363,7 @@ func Test_checkUpgradeVersion(t *testing.T) { init: func(term *terminal.Mock, cfg *config.Mock, vp *mockVersionProvider) { mockIsTTY(term, true) mockGetAndUpdateLastUpgradeCheck(cfg, "2099-01-27T11:55:26+01:00") - vp.mockVersions("2.0.0", "1.2.3") + vp.mockVersions(term, cfg, "2.0.0", "1.2.3") mockStopSpinner(term) mockConfirmUpgrade(term, "2.0.0", "1.2.3", true) }, diff --git a/pkg/commands/package_list/package-list.json b/pkg/commands/package_list/package-list.json index ca6025e..51ca71b 100644 --- a/pkg/commands/package_list/package-list.json +++ b/pkg/commands/package_list/package-list.json @@ -227,7 +227,7 @@ "url": "https://github.com/akamai/cli-terraform", "issues": "https://github.com/akamai/cli-terraform/issues", "commands": [{"name":"terraform","description":"Administer and Manage Akamai Terraform configurations","bin":"https://github.com/akamai/cli-terraform/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true,"ldflags": "-X 'github.com/akamai/cli-terraform/cli.Version=%s'"}], - "requirements": {"go":"1.22.0"} + "requirements": {"go":"1.23.6"} }, { "title": "Test Center", diff --git a/pkg/commands/subcommands.go b/pkg/commands/subcommands.go index 71715b7..0f9b60d 100644 --- a/pkg/commands/subcommands.go +++ b/pkg/commands/subcommands.go @@ -45,19 +45,19 @@ func readPackage(dir string) (subcommands, error) { if _, err := os.Stat(filepath.Join(dir, "cli.json")); err != nil { dir = filepath.Dir(dir) if _, err = os.Stat(filepath.Join(dir, "cli.json")); err != nil { - return subcommands{}, cli.Exit("Package does not contain a cli.json file.", 1) + return subcommands{}, fmt.Errorf("package does not contain a cli.json file: %v", err) } } var packageData subcommands cliJSON, err := os.ReadFile(filepath.Join(dir, "cli.json")) if err != nil { - return subcommands{}, err + return subcommands{}, fmt.Errorf("unable to read package: %v", err) } err = json.Unmarshal(cliJSON, &packageData) if err != nil { - return subcommands{}, err + return subcommands{}, fmt.Errorf("unable to unmarshal package: %v", err) } for key := range packageData.Commands { @@ -72,19 +72,19 @@ func readPackage(dir string) (subcommands, error) { func readPackageFromGithub(url, dir string) (subcommands, error) { response, err := http.Get(url) if err != nil { - return subcommands{}, err + return subcommands{}, fmt.Errorf("unable to get package from github: %v", err) } if response.StatusCode == http.StatusOK { cliJSON, err := io.ReadAll(response.Body) if err != nil { - return subcommands{}, err + return subcommands{}, fmt.Errorf("unable to read package: %v", err) } var packageData subcommands err = json.Unmarshal(cliJSON, &packageData) if err != nil { - return subcommands{}, err + return subcommands{}, fmt.Errorf("unable to unmarshal package: %v", err) } packageData.raw = cliJSON @@ -99,7 +99,7 @@ func readPackageFromGithub(url, dir string) (subcommands, error) { } - return subcommands{}, fmt.Errorf("Invalid response status while fetching cli.json: %d", response.StatusCode) + return subcommands{}, fmt.Errorf("invalid response status while fetching cli.json: %d", response.StatusCode) } func getPackagePaths() []string { @@ -148,18 +148,18 @@ func downloadBin(ctx context.Context, dir string, cmd command) error { cmd.Arch = runtime.GOARCH cmd.OS = runtime.GOOS - if runtime.GOOS == "darwin" { + if cmd.OS == "darwin" { cmd.OS = "mac" } - if runtime.GOOS == "windows" { + if cmd.OS == "windows" { cmd.BinSuffix = ".exe" } t := template.Must(template.New("url").Parse(cmd.Bin)) buf := &bytes.Buffer{} if err := t.Execute(buf, cmd); err != nil { - logger.Debug(fmt.Sprintf("Unable to create URL. Template: %s; Error: %s.", cmd.Bin, err.Error())) + logger.Error(fmt.Sprintf("Unable to create URL. Template: %s; Error: %v", cmd.Bin, err)) return err } @@ -169,25 +169,28 @@ func downloadBin(ctx context.Context, dir string, cmd command) error { binName := filepath.Join(dir, "akamai-"+strings.ToLower(cmd.Name)+cmd.BinSuffix) bin, err := os.Create(binName) if err != nil { + logger.Error(fmt.Sprintf("Unable to create %s file: %v", binName, err)) return err } defer func() { if err := bin.Close(); err != nil { - logger.Error(fmt.Sprintf("Error closing file: %s", err)) + logger.Error(fmt.Sprintf("Error closing file: %v", err)) } }() if err := os.Chmod(binName, 0775); err != nil { + logger.Error(fmt.Sprintf("Unable to change the %s file mode: %v", binName, err)) return err } res, err := http.Get(url) if err != nil { + logger.Error(fmt.Sprintf("Unable to get command binary: %v", err)) return err } defer func() { if err := res.Body.Close(); err != nil { - logger.Error(fmt.Sprintf("Error closing request body: %s", err)) + logger.Error(fmt.Sprintf("Error closing request body: %v", err)) } }() @@ -197,6 +200,7 @@ func downloadBin(ctx context.Context, dir string, cmd command) error { n, err := io.Copy(bin, res.Body) if err != nil || n == 0 { + logger.Error(fmt.Sprintf("Unable to copy from %s to %s: %v", res.Body, binName, err)) return err } diff --git a/pkg/commands/upgrade.go b/pkg/commands/upgrade.go index 639f843..9081f3a 100644 --- a/pkg/commands/upgrade.go +++ b/pkg/commands/upgrade.go @@ -56,8 +56,8 @@ func checkUpgradeVersion(ctx context.Context, force bool, provider versionProvid data, _ := cfg.GetValue("cli", "last-upgrade-check") data = strings.TrimSpace(data) - if data == "ignore" && !force { + logger.Error("Upgrade checks are disabled") return "" } @@ -69,8 +69,8 @@ func checkUpgradeVersion(ctx context.Context, force bool, provider versionProvid if !checkForUpgrade { configValue := strings.TrimPrefix(strings.TrimSuffix(data, "\""), "\"") lastUpgrade, err := time.Parse(time.RFC3339, configValue) - if err != nil { + logger.Error(fmt.Sprintf("Error parsing last upgrade check time: %v", err)) return "" } @@ -84,6 +84,7 @@ func checkUpgradeVersion(ctx context.Context, force bool, provider versionProvid cfg.SetValue("cli", "last-upgrade-check", time.Now().Format(time.RFC3339)) err := cfg.Save(ctx) if err != nil { + logger.Error(fmt.Sprintf("Error saving config: %v", err)) return "" } @@ -98,6 +99,7 @@ func checkUpgradeVersion(ctx context.Context, force bool, provider versionProvid color.BlueString(latestVersion), color.BlueString(currentVersion), ), true); err != nil || !answer { + logger.Error(fmt.Sprintf("Upgrade declined: %v", err)) return "" } return latestVersion @@ -127,15 +129,17 @@ func (p defaultVersionProvider) getLatestReleaseVersion(ctx context.Context) str } resp, err := client.Head(fmt.Sprintf("%s/releases/latest", repo)) if err != nil { + logger.Error(fmt.Sprintf("Error checking for latest version: %v", err)) return "0" } defer func() { if err := resp.Body.Close(); err != nil { - logger.Error(err.Error()) + logger.Error(fmt.Sprintf("Error closing response body: %v", err)) } }() if resp.StatusCode != http.StatusFound { + logger.Error(fmt.Sprintf("Error checking for latest version: %s", resp.Status)) return "0" } diff --git a/pkg/commands/upgrade_common.go b/pkg/commands/upgrade_common.go index 5d4edba..030cac0 100644 --- a/pkg/commands/upgrade_common.go +++ b/pkg/commands/upgrade_common.go @@ -34,7 +34,7 @@ func UpgradeCli(ctx context.Context, latestVersion string) (e error) { logger.Debug(fmt.Sprintf("UPGRADE FINISH: %v", time.Since(start))) } else { term.Spinner().Fail() - logger.Error(fmt.Sprintf("UPGRADE ERROR: %v", e.Error())) + logger.Error(fmt.Sprintf("UPGRADE ERROR: %v", e)) } }() @@ -65,6 +65,7 @@ func UpgradeCli(ctx context.Context, latestVersion string) (e error) { resp, err := http.Get(buf.String()) if err != nil || resp.StatusCode != http.StatusOK { + logger.Error(fmt.Sprintf("Unable to get release: %s", err)) var reason string if err == nil { reason = fmt.Sprintf("%s: %s", buf.String(), resp.Status) diff --git a/pkg/packages/golang.go b/pkg/packages/golang.go index ea48024..246646e 100644 --- a/pkg/packages/golang.go +++ b/pkg/packages/golang.go @@ -35,8 +35,10 @@ import ( func (l *langManager) installGolang(ctx context.Context, dir, ver string, commands, ldFlags []string) error { logger := log.FromContext(ctx) + goBin, err := l.commandExecutor.LookPath("go") if err != nil { + logger.Error("Go executable not found") return fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "go") } @@ -50,6 +52,7 @@ func (l *langManager) installGolang(ctx context.Context, dir, ver string, comman matches := r.FindStringSubmatch(string(output)) if len(matches) == 0 { + logger.Error(fmt.Sprintf("Unable to determine Go version: %s", string(output))) return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "go", ver) } @@ -60,15 +63,19 @@ func (l *langManager) installGolang(ctx context.Context, dir, ver string, comman } cliPath, err := tools.GetAkamaiCliPath() - if goPath := os.Getenv("GOPATH"); goPath != "" { - cliPath = fmt.Sprintf("%s%d%s", goPath, os.PathListSeparator, cliPath) - } if err != nil { return cli.Exit(color.RedString("Unable to determine CLI home directory"), 1) } + + if goPath := os.Getenv("GOPATH"); goPath != "" { + cliPath = fmt.Sprintf("%s%d%s", goPath, os.PathListSeparator, cliPath) + } + if err := os.Setenv("GOPATH", cliPath); err != nil { + logger.Error(fmt.Sprintf("Unable to set GOPATH: %v", err)) return err } + if err = installGolangModules(logger, l.commandExecutor, dir); err != nil { return err } diff --git a/pkg/packages/javascript.go b/pkg/packages/javascript.go index 63dd2e3..5ce01b6 100644 --- a/pkg/packages/javascript.go +++ b/pkg/packages/javascript.go @@ -34,6 +34,7 @@ func (l *langManager) installJavaScript(ctx context.Context, dir, ver string) er if err != nil { bin, err = l.commandExecutor.LookPath("nodejs") if err != nil { + logger.Error("Node.js executable not found") return fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "Node.js") } } @@ -68,9 +69,12 @@ func installNodeDepsYarn(ctx context.Context, cmdExecutor executor, dir string) logger := log.FromContext(ctx) if ok, _ := cmdExecutor.FileExists(filepath.Join(dir, "yarn.lock")); !ok { + logger.Debug("yarn.lock not found") return nil } + logger.Info("yarn.lock found, running yarn package manager") + bin, err := cmdExecutor.LookPath("yarn") if err == nil { cmd := exec.Command(bin, "install") @@ -85,6 +89,7 @@ func installNodeDepsYarn(ctx context.Context, cmdExecutor executor, dir string) } return nil } + err = fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "yarn") logger.Debug(err.Error()) return err @@ -116,5 +121,4 @@ func installNodeDepsNpm(ctx context.Context, cmdExecutor executor, dir string) e err = fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "npm") logger.Debug(err.Error()) return err - } diff --git a/pkg/packages/php.go b/pkg/packages/php.go index e225697..7875e93 100644 --- a/pkg/packages/php.go +++ b/pkg/packages/php.go @@ -27,13 +27,14 @@ import ( ) func (l *langManager) installPHP(ctx context.Context, dir, cmdReq string) error { + logger := log.FromContext(ctx) + bin, err := l.commandExecutor.LookPath("php") if err != nil { + logger.Error(fmt.Sprintf("PHP binary not found: %v", err)) return fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "php") } - logger := log.FromContext(ctx) - logger.Debug(fmt.Sprintf("PHP binary found: %s", bin)) if cmdReq != "" && cmdReq != "*" { @@ -60,6 +61,7 @@ func installPHPDepsComposer(ctx context.Context, cmdExecutor executor, phpBin, d logger := log.FromContext(ctx) if ok, _ := cmdExecutor.FileExists(filepath.Join(dir, "composer.json")); !ok { + logger.Debug("composer.json not found") return nil } logger.Info("composer.json found, running composer package manager") diff --git a/pkg/packages/python.go b/pkg/packages/python.go index 037029f..0f5f25e 100644 --- a/pkg/packages/python.go +++ b/pkg/packages/python.go @@ -43,16 +43,17 @@ var ( func (l *langManager) installPython(ctx context.Context, venvPath, srcPath, requiredPy string) error { logger := log.FromContext(ctx) + logger.Debug("Starting Python installation") pythonBin, pipBin, err := l.validatePythonDeps(ctx, logger, requiredPy, filepath.Base(srcPath)) if err != nil { - logger.Error(fmt.Sprintf("%v", err)) - return err + logger.Error(fmt.Sprintf("Dependency validation failed: %v", err)) + return fmt.Errorf("unable to validate python dependency: %v", err) } if err = l.setup(ctx, venvPath, srcPath, pythonBin, pipBin, requiredPy, false); err != nil { - logger.Error(fmt.Sprintf("%v", err)) - return err + logger.Error(fmt.Sprintf("Setup failed: %v", err)) + return fmt.Errorf("unable to setup python virutalenv for the given module: %v", err) } return nil @@ -60,11 +61,12 @@ func (l *langManager) installPython(ctx context.Context, venvPath, srcPath, requ // setup does the python virtualenv set up for the given module. It may return an error. func (l *langManager) setup(ctx context.Context, pkgVenvPath, srcPath, python3Bin, pipBin, requiredPy string, passthru bool) error { + logger := log.FromContext(ctx) + logger.Debug("Setting up Python environment") + switch version.Compare(requiredPy, "3.0.0") { case version.Greater, version.Equals: // Python 3.x required: build virtual environment - logger := log.FromContext(ctx) - defer func() { if !passthru { l.deactivateVirtualEnvironment(ctx, pkgVenvPath, requiredPy) @@ -74,11 +76,12 @@ func (l *langManager) setup(ctx context.Context, pkgVenvPath, srcPath, python3Bi veExists, err := l.commandExecutor.FileExists(pkgVenvPath) if err != nil { + logger.Error(fmt.Sprintf("Failed to check package virtualenv existence: %v", err)) return err } if !passthru || !veExists { - logger.Debug(fmt.Sprintf("the virtual environment %s does not exist yet - installing dependencies", pkgVenvPath)) + logger.Debug(fmt.Sprintf("The virtual environment %s does not exist yet - installing dependencies", pkgVenvPath)) // upgrade pip and setuptools if err := l.upgradePipAndSetuptools(ctx, python3Bin); err != nil { @@ -99,6 +102,7 @@ func (l *langManager) setup(ctx context.Context, pkgVenvPath, srcPath, python3Bi // install packages from requirements.txt vePy, err := l.getVePython(pkgVenvPath) if err != nil { + logger.Error(fmt.Sprintf("Failed to get virtualenv python executable: %v", err)) return err } if err := l.installVeRequirements(ctx, srcPath, pkgVenvPath, vePy); err != nil { @@ -128,13 +132,15 @@ func (l *langManager) installVeRequirements(ctx context.Context, srcPath, vePath requirementsPath := filepath.Join(srcPath, "requirements.txt") if ok, _ := l.commandExecutor.FileExists(requirementsPath); !ok { + logger.Error("requirements.txt not found") return ErrRequirementsTxtNotFound } logger.Info("requirements.txt found, running pip package manager") shell, err := l.GetShell(l.GetOS()) if err != nil { - return err + logger.Error("cannot determine OS shell") + return fmt.Errorf("unable to determine OS shell: %v", err) } if shell == "" { // windows @@ -148,6 +154,8 @@ func (l *langManager) installVeRequirements(ctx context.Context, srcPath, vePath logger.Error("failed to run pip install --upgrade --ignore-installed -r requirements.txt") return fmt.Errorf("%w: %s", ErrRequirementsInstall, string(output)) } + logger.Debug("Python virtualenv requirements successfully installed") + return nil } @@ -160,6 +168,8 @@ func (l *langManager) installVeRequirements(ctx context.Context, srcPath, vePath return fmt.Errorf("%w: %v", ErrRequirementsInstall, string(output)) } + logger.Debug("Python virtualenv requirements successfully installed") + return nil } @@ -191,6 +201,7 @@ func (l *langManager) validatePythonDeps(ctx context.Context, logger *slog.Logge pipBin, err := findPipBin(ctx, l.commandExecutor, requiredPy) if err != nil { + logger.Error("Pip not found for python 2.x module. Please verify your setup") return pythonBin, "", err } return pythonBin, pipBin, nil @@ -265,7 +276,7 @@ func (l *langManager) activateVirtualEnvironment(ctx context.Context, pkgVenvPat interpreter, err := l.GetShell(oS) if err != nil { logger.Error("cannot determine OS shell") - return err + return fmt.Errorf("unable to determine OS shell: %v", err) } cmd := &exec.Cmd{} if oS == "windows" { @@ -286,7 +297,6 @@ func (l *langManager) activateVirtualEnvironment(ctx context.Context, pkgVenvPat } func (l *langManager) deactivateVirtualEnvironment(ctx context.Context, dir, pyVersion string) { - compare := version.Compare(pyVersion, "3.0.0") if compare == version.Equals || compare == version.Greater { logger := log.FromContext(ctx) @@ -313,11 +323,15 @@ func (l *langManager) deactivateVirtualEnvironment(ctx context.Context, dir, pyV func (l *langManager) resolveBinVersion(bin, cmdReq, arg string, logger *slog.Logger) error { cmd := exec.Command(bin, arg) + logger.Debug("Resolving python version") + output, err := l.commandExecutor.ExecCommand(cmd, true) if err != nil { - return err + logger.Error(fmt.Sprintf("Failed to execute %s command: %v", cmd.Path, err)) + return fmt.Errorf("command %s execution failed: %v", cmd.Path, err) } logger.Debug(fmt.Sprintf("%s %s: %s", bin, arg, bytes.ReplaceAll(output, []byte("\n"), []byte("")))) + matches := pythonVersionRegex.FindStringSubmatch(string(output)) if len(matches) < 2 { return fmt.Errorf("%w: %s: %s", ErrRuntimeNoVersionFound, "python", cmd) @@ -338,6 +352,7 @@ func (l *langManager) resolveBinVersion(bin, cmdReq, arg string, logger *slog.Lo case version.Equals: return nil } + return ErrPythonVersionNotSupported } @@ -364,6 +379,7 @@ func (l *langManager) upgradePipAndSetuptools(ctx context.Context, python3Bin st logger.Error(fmt.Sprintf("%v: %s", ErrPipSetuptoolsUpgrade, string(output))) return fmt.Errorf("%w: %s", ErrPipSetuptoolsUpgrade, string(output)) } + logger.Debug("pip & setuptools successfully installed/upgraded") return nil } @@ -382,6 +398,7 @@ func (l *langManager) createVirtualEnvironment(ctx context.Context, python3Bin s logger.Debug(fmt.Sprintf("%s directory created", venvPath)) } else { if err != nil { + logger.Error(fmt.Sprintf("Failed to check package virtualenv existence: %v", err)) return err } } @@ -399,6 +416,7 @@ func (l *langManager) createVirtualEnvironment(ctx context.Context, python3Bin s func findPythonBin(ctx context.Context, cmdExecutor executor, ver, name string) (string, error) { logger := log.FromContext(ctx) + logger.Debug("Looking for python binaries") var err error var bin string @@ -470,11 +488,13 @@ func installPythonDepsPip(ctx context.Context, cmdExecutor executor, bin, dir st logger := log.FromContext(ctx) if ok, _ := cmdExecutor.FileExists(filepath.Join(dir, "requirements.txt")); !ok { + logger.Debug("requirements.txt not found") return nil } logger.Info("requirements.txt found, running pip package manager") if err := os.Setenv("PYTHONUSERBASE", dir); err != nil { + logger.Error(fmt.Sprintf("Failed to set environment for the key %s in %s: %v", "PYTHONUSERBASE", dir, err)) return err } args := []string{bin, "install", "--user", "--ignore-installed", "-r", filepath.Join(dir, "requirements.txt")} @@ -487,6 +507,8 @@ func installPythonDepsPip(ctx context.Context, cmdExecutor executor, bin, dir st } return fmt.Errorf("%w: %s. Please verify pip system dependencies (setuptools, python3-dev, gcc, libffi-dev, openssl-dev)", ErrPackageManagerExec, "pip") } + logger.Debug(fmt.Sprintf("Python dependencies successfully installed using pip: %s", strings.Join(args, " "))) + return nil } diff --git a/pkg/packages/python_test.go b/pkg/packages/python_test.go index 2a413c3..ddbef24 100644 --- a/pkg/packages/python_test.go +++ b/pkg/packages/python_test.go @@ -160,7 +160,7 @@ venv: error: the following arguments are required: ENV_DIR m.On("LookPath", "py.exe").Return("", errors.New("")).Once() m.On("LookPath", "python3.exe").Return("", errors.New("")).Once() }, - withError: ErrRuntimeNotFound, + withError: fmt.Errorf("unable to validate python dependency: unable to locate runtime: python 3. Please verify if the executable is included in your PATH"), }, "without python 2, python 2 required": { givenDir: srcDir, @@ -170,7 +170,7 @@ venv: error: the following arguments are required: ENV_DIR m.On("LookPath", "py.exe").Return("", errors.New("")).Once() m.On("LookPath", "python2.exe").Return("", errors.New("")).Once() }, - withError: ErrRuntimeNotFound, + withError: fmt.Errorf("unable to validate python dependency: unable to locate runtime: python 2. Please verify if the executable is included in your PATH"), }, "with python 3.4 and pip, python 3 required": { givenDir: srcDir, @@ -372,7 +372,7 @@ venv: error: the following arguments are required: ENV_DIR givenDir: srcDir, veDir: veDir, init: func(_ *mocked) {}, - withError: ErrPythonVersionNotSupported, + withError: fmt.Errorf("unable to validate python dependency: python version not supported: "), }, "version not found": { givenDir: srcDir, @@ -385,7 +385,7 @@ venv: error: the following arguments are required: ENV_DIR Args: []string{py3Bin, "--version"}, }, true).Return([]byte{}, nil).Once() }, - withError: ErrRuntimeNoVersionFound, + withError: fmt.Errorf("unable to validate python dependency: unable to determine installed version, minimum version required: python: /test/python3 --version"), }, "version too low": { givenDir: srcDir, @@ -398,7 +398,7 @@ venv: error: the following arguments are required: ENV_DIR Args: []string{py3Bin, "--version"}, }, true).Return([]byte(py34Version), nil).Once() }, - withError: ErrRuntimeMinimumVersionRequired, + withError: fmt.Errorf("unable to validate python dependency: higher version is required to install this command: required: /test/python3:3.5.5, have: 3.4.0. Please install the required Python branch"), }, "python 2 required, pip2 bin not found": { givenDir: srcDir, @@ -411,7 +411,7 @@ venv: error: the following arguments are required: ENV_DIR Args: []string{py2Bin, "--version"}, }, true).Return([]byte(py2Version), nil).Once() }, - withError: ErrPackageManagerNotFound, + withError: fmt.Errorf("unable to validate python dependency: unable to locate package manager in PATH, pip2"), }, "python 2 required, just python 3 is installed": { givenDir: srcDir, @@ -422,7 +422,7 @@ venv: error: the following arguments are required: ENV_DIR m.On("LookPath", "py.exe").Return(py2Bin, nil).Once() m.On("ExecCommand", &exec.Cmd{Path: py2Bin, Args: []string{"/test/python2", "--version"}}, true).Return([]byte(py34Version), nil).Once() }, - withError: ErrRuntimeNotFound, + withError: fmt.Errorf("unable to validate python dependency: unable to locate runtime: Please install the following Python branch: 2.0.0"), }, } @@ -434,7 +434,7 @@ venv: error: the following arguments are required: ENV_DIR err := l.installPython(context.Background(), test.veDir, test.givenDir, test.requiredPy) m.AssertExpectations(t) if test.withError != nil { - assert.True(t, errors.Is(err, test.withError), "want: %s; got: %s", test.withError, err) + assert.Equal(t, test.withError.Error(), err.Error(), "want: %s; got: %s", test.withError, err) return } require.NoError(t, err) diff --git a/pkg/packages/ruby.go b/pkg/packages/ruby.go index 1113cdc..fab7bae 100644 --- a/pkg/packages/ruby.go +++ b/pkg/packages/ruby.go @@ -32,6 +32,7 @@ func (l *langManager) installRuby(ctx context.Context, dir, cmdReq string) error bin, err := l.commandExecutor.LookPath("ruby") if err != nil { + logger.Error("Ruby executable not found") return fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "ruby") } @@ -45,6 +46,7 @@ func (l *langManager) installRuby(ctx context.Context, dir, cmdReq string) error matches := r.FindStringSubmatch(string(output)) if len(matches) == 0 { + logger.Error(fmt.Sprintf("Unable to determine Ruby version: %s", output)) return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "ruby", cmdReq) } @@ -61,8 +63,10 @@ func installRubyDepsBundler(ctx context.Context, cmdExecutor executor, dir strin logger := log.FromContext(ctx) if ok, _ := cmdExecutor.FileExists(filepath.Join(dir, "Gemfile")); !ok { + logger.Debug("Gemfile not found") return nil } + logger.Debug("Gemfile found, running yarn package manager") bin, err := cmdExecutor.LookPath("bundle") if err == nil { diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index 247912d..09c9bdc 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -146,7 +146,11 @@ func (t *DefaultTerminal) WriteErrorf(f string, args ...interface{}) { // WriteError write a message to the error stream func (t *DefaultTerminal) WriteError(v interface{}) { - _, _ = fmt.Fprintf(t.err, v.(string)) + message := v.(string) + if !strings.HasSuffix(message, "\n") { + message += "\n" + } + _, _ = fmt.Fprint(t.err, message) } // Error return the error writer diff --git a/pkg/terminal/terminal_test.go b/pkg/terminal/terminal_test.go index 3a5bb97..6d82b45 100644 --- a/pkg/terminal/terminal_test.go +++ b/pkg/terminal/terminal_test.go @@ -108,7 +108,23 @@ func TestWriteError(t *testing.T) { data, err := io.ReadAll(out) require.NoError(t, err) - assert.Equal(t, t.Name(), string(data)) + assert.Equal(t, t.Name()+"\n", string(data)) + + // Error message with "\n" + err = out.Truncate(0) + require.NoError(t, err) + _, err = out.Seek(0, 0) + require.NoError(t, err) + + term.WriteError("test error\n") + + _, err = out.Seek(0, 0) + require.NoError(t, err) + + data, err = io.ReadAll(out) + require.NoError(t, err) + + assert.Equal(t, "test error\n", string(data)) } func TestWriteErrorf(t *testing.T) { diff --git a/pkg/version/version.go b/pkg/version/version.go index 69f0e2d..cf1c062 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -4,7 +4,7 @@ import "github.com/Masterminds/semver" const ( // Version Application Version - Version = "2.0.0" + Version = "2.0.1" // Equals p1==p2 in version.Compare(p1, p2) Equals = 0 // Error failure parsing one of the parameters in version.Compare(p1, p2)