diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..c81f9cf
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,42 @@
+linters:
+ disable-all: true
+ enable:
+ - revive
+
+output:
+ sort-results: true
+
+issues:
+ exclude-use-default: false
+ max-issues-per-linter: 0
+ max-same-issues: 0
+
+linters-settings:
+ revive:
+ ignore-generated-header: true
+ severity: warning
+ rules:
+ - name: blank-imports
+ - name: context-as-argument
+ - name: context-keys-type
+ - name: dot-imports
+ - name: error-return
+ - name: error-strings
+ - name: error-naming
+ - name: exported
+ - name: if-return
+ - name: increment-decrement
+ - name: var-naming
+ - name: var-declaration
+ - name: package-comments
+ - name: range
+ - name: receiver-naming
+ - name: time-naming
+ - name: unexported-return
+ - name: indent-error-flow
+ - name: errorf
+ - name: empty-block
+ - name: superfluous-else
+ - name: unused-parameter
+ - name: unreachable-code
+ - name: redefines-builtin-id
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65b9732..983c366 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,20 @@
+# 1.4.0 (March 14, 2022)
+
+## Enhancements
+
+* [IMPORTANT] Refactor Python support, making use of virtual environments to isolate dependencies for each Python package.
+ * Refer to README.md for new system dependencies.
+
# 1.3.1 (December 8, 2021)
## Enhancements
+
* Improved message for updating CLI version
# 1.3.0 (October 6, 2021)
## Fixes
+
* Remove old binary in PowerShell terminal ([#125](https://github.com/akamai/cli/issues/125)).
* Document CLI exit codes.
* Review exit code when trying to install an already installed command ([#83](https://github.com/akamai/cli/issues/83)).
diff --git a/Makefile b/Makefile
index 93af0cc..4942902 100644
--- a/Makefile
+++ b/Makefile
@@ -1,41 +1,102 @@
+BIN = $(CURDIR)/bin
+GOCMD = go
+GOTEST = $(GOCMD) test
+GOBUILD = $(GOCMD) build
+M = $(shell echo ">")
+
+$(BIN):
+ @mkdir -p $@
+$(BIN)/%: | $(BIN) ; $(info $(M) Installing $(PACKAGE)...)
+ @tmp=$$(mktemp -d); \
+ env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GOCMD) get $(PACKAGE) \
+ || ret=$$?; \
+ rm -rf $$tmp ; exit $$ret
+
+GOIMPORTS = $(BIN)/goimports
+$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports
+
+GOCOV = $(BIN)/gocov
+$(BIN)/gocov: PACKAGE=github.com/axw/gocov/...
+
+GOCOVXML = $(BIN)/gocov-xml
+$(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml
+
+GOJUNITREPORT = $(BIN)/go-junit-report
+$(BIN)/go-junit-report: PACKAGE=github.com/jstemmer/go-junit-report
+
+GOLANGCILINT = $(BIN)/golangci-lint
+GOLANGCI_LINT_VERSION = v1.41.1
+$(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)
+
+COVERAGE_MODE = atomic
+COVERAGE_DIR = $(CURDIR)/test/coverage
+COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out
+COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml
+COVERAGE_HTML = $(COVERAGE_DIR)/index.html
+
.PHONY: all
-all: fmt lint vet coverage
+all: clean fmt-check lint coverage create-junit-report create-coverage-files clean-tools
.PHONY: build
-build:
- go build -o akamai cli/main.go
+build: ; $(info $(M) Building 'akamai' binary...) @ ## Build the binary from source
+ $(GOBUILD) -o $(CURDIR)/akamai cli/main.go
.PHONY: test
-test:
- go test -count=1 ./...
-
-.PHONY: coverage-ui
-coverage-ui:
- go test -covermode=count -coverprofile=coverage.out ./...
- go tool cover -html=coverage.out
+test: ; $(info $(M) Running tests...) ## Run all unit tests
+ $(GOTEST) -count=1 ./...
.PHONY: coverage
-coverage:
- go test -coverprofile coverage.out ./...
- go tool cover -func coverage.out | grep total
+coverage: ; $(info $(M) Running tests with coverage...) @ ## Run tests and generate coverage profile
+ @mkdir -p $(COVERAGE_DIR)
+ @$(GOTEST) -v -covermode=$(COVERAGE_MODE) \
+ -coverprofile="$(COVERAGE_PROFILE)" ./... | tee test/tests.output
-.PHONY: lint
-lint:
- golint -set_exit_status ./...
+.PHONY: create-junit-report
+create-junit-report: | $(GOJUNITREPORT) ; $(info $(M) Creating juint xml report) @ ## Generate junit-style coverage report
+ @cat $(CURDIR)/test/tests.output | $(GOJUNITREPORT) > $(CURDIR)/test/tests.xml
+ @sed -i -e 's/skip=/skipped=/g;s/ failures=/ errors="0" failures=/g' $(CURDIR)/test/tests.xml
+
+.PHONY: create-coverage-files
+create-coverage-files: | $(GOCOV) $(GOCOVXML); $(info $(M) Creating coverage files...) @ ## Generate coverage report files
+ @$(GOCMD) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML)
+ @$(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML)
-.PHONY: vet
-vet:
- go vet ./...
+.PHONY: lint
+lint: | $(GOLANGCILINT); $(info $(M) Running linter...) @ ## Run golangci-lint on all source files
+ @$(BIN)/golangci-lint run
.PHONY: fmt
-fmt:
- go fmt ./...
+fmt: | $(GOIMPORTS); $(info $(M) Running goimports...) @ ## Run goimports on all source files
+ @$(GOIMPORTS) -w .
-.PHONY: release
-release:
- ./build.sh
+.PHONY: fmt-check
+fmt-check: | $(GOIMPORTS); $(info $(M) Running format and imports check...) @ ## Run goimports on all source files (do not modify files)
+ $(eval OUTPUT = $(shell $(GOIMPORTS) -l .))
+ @if [ "$(OUTPUT)" != "" ]; then\
+ echo "Found following files with incorrect format and/or imports:";\
+ echo "$(OUTPUT)";\
+ false;\
+ fi
+.PHONY: release
+release: ; $(info $(M) Generating release binaries and signatures...) @ ## Generate release binaries
+ @./build.sh
.PHONY: pack
-pack:
- tar -zcvf cli.tar.gz .
\ No newline at end of file
+pack: ; $(info $(M) Generating tarball...) @ ## Create cli tarball
+ @tar -zcf cli.tar.gz *
+
+.PHONY: ; clean
+clean: ; $(info $(M) Removing 'bin' directory and test results...) @ ## Cleanup installed packages and test reports
+ @rm -rf $(BIN)
+ @rm -rf $(BIN)/test/tests.* $(BIN)/test/coverage
+
+clean-tools: ## Cleanup installed packages
+ @rm -rf $(BIN)/go*
+
+.PHONY: help
+help: ## List all make targets
+ echo $(MAKEFILE_LIST)
+ @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'
diff --git a/README.md b/README.md
index cfb93da..a09eda6 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
-
+
@@ -29,6 +29,15 @@ Install Akamai CLI by downloading a [release binary](https://github.com/akamai/c
You can also use [Homebrew](#install-with-homebrew), [Docker](#install-with-docker), or compile from [source](#compile-from-source).
+### System dependencies for Python-based packages
+
+If you're using a Python-based CLI package, install these extra dependencies:
+
+- Python 3.3 or above
+- [Python 3 `pip` package installer](https://pip.pypa.io/en/stable/installation)
+- [Python 3 `venv` module](https://docs.python.org/3/library/venv.html)
+- Up-to-date common CA certificates for your operating system (PEM files)
+
### Install from binaries
Follow the instructions for your operating system.
@@ -284,16 +293,16 @@ The package you install needs a `cli.json` file. This is where you specify the c
- `commands`: Lists commands included in the package.
- `name`: The command name, used as the executable name.
- `aliases`: An array of aliases that invoke the same command.
- - `version`: The command version.
+ - `version`: The command version.
- `description`: A short description for the command.
- `bin`: A URL to fetch a binary package from if it cannot be installed from source.
The `bin` URL may contain the following placeholders:
- - `{{.Version}}`: The command version.
- - `{{.Name}}`: The command name.
+ - `{{.Version}}`: The command version.
+ - `{{.Name}}`: The command name.
- `{{.OS}}`: The current operating system, either `windows`, `mac`, or `linux`.
- - `{{.Arch}}`: The current OS architecture, either `386` or `amd64`.
+ - `{{.Arch}}`: The current OS architecture, either `386` or `amd64`.
- `{{.BinSuffix}}`: The binary suffix for the current OS: `.exe` for `windows`.
### Example
diff --git a/assets/package-list.json b/assets/package-list.json
index 6d0a4e1..9b23e99 100644
--- a/assets/package-list.json
+++ b/assets/package-list.json
@@ -103,13 +103,24 @@
{
- "title": "Certificate Provisioning Service (CPS)",
- "name": "cps",
- "version": "v1.0.8",
- "url": "https://github.com/akamai/cli-cps",
- "issues": "https://github.com/akamai/cli-cps/issues",
- "commands": [{"name":"cps","aliases":["certs"],"version":"1.0.8","description":"Access Certificate Provisioning System (CPS) Information"}],
- "requirements": {"python":"3.0.0"}
+ "title": "Certificate Provisioning Service (CPS)",
+ "name": "cps",
+ "version": "v1.0.9",
+ "url": "https://github.com/akamai/cli-cps",
+ "issues": "https://github.com/akamai/cli-cps/issues",
+ "commands": [
+ {
+ "name": "cps",
+ "aliases": [
+ "certs"
+ ],
+ "version": "1.0.9",
+ "description": "Access Certificate Provisioning System (CPS) Information"
+ }
+ ],
+ "requirements": {
+ "python": "3.0.0"
+ }
},
diff --git a/cli/app/access_nix.go b/cli/app/access_nix.go
index 3043c65..8159470 100644
--- a/cli/app/access_nix.go
+++ b/cli/app/access_nix.go
@@ -1,3 +1,4 @@
+//go:build !windows
// +build !windows
// Copyright 2018. Akamai Technologies, Inc
diff --git a/cli/app/firstrun.go b/cli/app/firstrun.go
index f155331..55f9763 100644
--- a/cli/app/firstrun.go
+++ b/cli/app/firstrun.go
@@ -1,4 +1,5 @@
-//+build !nofirstrun
+//go:build !nofirstrun
+// +build !nofirstrun
// Copyright 2018. Akamai Technologies, Inc
//
@@ -23,14 +24,12 @@ import (
"runtime"
"strings"
- "github.com/fatih/color"
- "github.com/kardianos/osext"
-
"github.com/akamai/cli/pkg/config"
-
"github.com/akamai/cli/pkg/stats"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
+ "github.com/fatih/color"
+ "github.com/kardianos/osext"
)
const (
diff --git a/cli/app/firstrun_noinstall.go b/cli/app/firstrun_noinstall.go
index 2eb7b5c..90ad63a 100644
--- a/cli/app/firstrun_noinstall.go
+++ b/cli/app/firstrun_noinstall.go
@@ -1,4 +1,5 @@
-//+build nofirstrun
+//go:build nofirstrun
+// +build nofirstrun
// Copyright 2018. Akamai Technologies, Inc
//
@@ -18,6 +19,7 @@ package app
import (
"context"
+
"github.com/akamai/cli/pkg/stats"
)
diff --git a/cli/app/run.go b/cli/app/run.go
index 8cde436..4c62f69 100644
--- a/cli/app/run.go
+++ b/cli/app/run.go
@@ -102,18 +102,12 @@ func findCollisions(availableCmds []*cli.Command, args []string) error {
metaCmds := []string{"help", "uninstall", "update"}
for _, c := range metaCmds {
if c == args[1] && len(args) > 2 {
- if err := findDuplicate(availableCmds, args[2]); err != nil {
- return err
- }
-
- return nil
+ return findDuplicate(availableCmds, args[2])
}
}
// rest of commands: we need to check the first parameter (args[1])
- if err := findDuplicate(availableCmds, args[1]); err != nil {
- return err
- }
+ return findDuplicate(availableCmds, args[1])
}
return nil
diff --git a/pkg/app/cli.go b/pkg/app/cli.go
index f6ca271..78e3771 100644
--- a/pkg/app/cli.go
+++ b/pkg/app/cli.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
+ "path"
"strings"
"time"
@@ -13,42 +14,16 @@ import (
"github.com/fatih/color"
"github.com/kardianos/osext"
+ "github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2"
)
-const sleepTime24Hours = time.Hour * 24
+const sleep24HDuration = time.Hour * 24
// CreateApp creates and sets up *cli.App
func CreateApp(ctx context.Context) *cli.App {
- term := terminal.Get(ctx)
-
- appName := "akamai"
- app := cli.NewApp()
- app.Name = appName
- app.HelpName = appName
- app.Usage = "Akamai CLI"
- app.Version = version.Version
- app.Writer = term
- app.ErrWriter = term.Error()
- app.Copyright = "Copyright (C) Akamai Technologies, Inc"
- app.EnableBashCompletion = true
- app.BashComplete = DefaultAutoComplete
- cli.VersionFlag = &cli.BoolFlag{
- Name: "version",
- Usage: "Output CLI version",
- }
-
- cli.BashCompletionFlag = &cli.BoolFlag{
- Name: "generate-auto-complete",
- Hidden: true,
- }
- cli.HelpFlag = &cli.BoolFlag{
- Name: "help",
- Usage: "show help",
- }
-
- SetHelpTemplates()
- app.Flags = []cli.Flag{
+ app := createAppTemplate(ctx, "", "Akamai CLI", "", version.Version, false)
+ app.Flags = append(app.Flags,
&cli.BoolFlag{
Name: "bash",
Usage: "Output bash auto-complete",
@@ -67,17 +42,7 @@ func CreateApp(ctx context.Context) *cli.App {
Hidden: true,
EnvVars: []string{"AKAMAI_CLI_DAEMON"},
},
- &cli.StringFlag{
- Name: "edgerc",
- Usage: "edgerc config path passed to executed commands, defaults to ~/.edgerc",
- Aliases: []string{"e"},
- },
- &cli.StringFlag{
- Name: "section",
- Usage: "edgerc section name passed to executed commands, defaults to 'default'",
- Aliases: []string{"s"},
- },
- }
+ )
app.Action = func(c *cli.Context) error {
return defaultAction(c)
@@ -99,7 +64,7 @@ func CreateApp(ctx context.Context) *cli.App {
if c.IsSet("daemon") {
for {
- time.Sleep(sleepTime24Hours)
+ time.Sleep(sleep24HDuration)
}
}
return nil
@@ -108,6 +73,85 @@ func CreateApp(ctx context.Context) *cli.App {
return app
}
+// CreateAppTemplate creates a basic *cli.App template
+func CreateAppTemplate(ctx context.Context, commandName, usage, description, version string) *cli.App {
+ return createAppTemplate(ctx, commandName, usage, description, version, true)
+}
+
+func createAppTemplate(ctx context.Context, commandName, usage, description, version string, useDefaults bool) *cli.App {
+ _, inCli := os.LookupEnv("AKAMAI_CLI")
+ term := terminal.Get(ctx)
+
+ appName := "akamai"
+ if commandName != "" {
+ appName = "akamai-" + commandName
+ if inCli {
+ appName = "akamai " + commandName
+ }
+ }
+
+ app := cli.NewApp()
+ app.Name = appName
+ app.HelpName = appName
+ app.Usage = usage
+ app.Description = description
+ app.Version = version
+
+ app.Copyright = "Copyright (C) Akamai Technologies, Inc"
+ app.Writer = term
+ app.ErrWriter = term.Error()
+ app.EnableBashCompletion = true
+ app.BashComplete = DefaultAutoComplete
+
+ var edgercpath, section string
+ if useDefaults {
+ edgercpath, _ = homedir.Dir()
+ edgercpath = path.Join(edgercpath, ".edgerc")
+
+ section = "default"
+ }
+
+ app.Flags = []cli.Flag{
+ &cli.StringFlag{
+ Name: "edgerc",
+ Aliases: []string{"e"},
+ Usage: "Location of the credentials file",
+ Value: edgercpath,
+ EnvVars: []string{"AKAMAI_EDGERC"},
+ },
+ &cli.StringFlag{
+ Name: "section",
+ Aliases: []string{"s"},
+ Usage: "Section of the credentials file",
+ Value: section,
+ EnvVars: []string{"AKAMAI_EDGERC_SECTION"},
+ },
+ &cli.StringFlag{
+ Name: "accountkey",
+ Aliases: []string{"account-key"},
+ Usage: "Account switch key",
+ EnvVars: []string{"AKAMAI_EDGERC_ACCOUNT_KEY"},
+ },
+ }
+
+ cli.VersionFlag = &cli.BoolFlag{
+ Name: "version",
+ Usage: "Output CLI version",
+ }
+ cli.BashCompletionFlag = &cli.BoolFlag{
+ Name: "generate-auto-complete",
+ Hidden: true,
+ }
+ cli.HelpFlag = &cli.BoolFlag{
+ Name: "help",
+ Usage: "show help",
+ }
+
+ SetHelpTemplates()
+
+ return app
+}
+
// DefaultAutoComplete ...
func DefaultAutoComplete(ctx *cli.Context) {
term := terminal.Get(ctx.Context)
@@ -187,7 +231,7 @@ func SetHelpTemplates() {
"{{if .Commands}} command [command flags]{{end}} "+
"{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}"+
"\n\n{{end}}") +
- "{{if .Description}}\n\n" +
+ "{{if .Description}}" +
color.YellowString("Description:\n") +
" {{.Description}}" +
"\n\n{{end}}" +
diff --git a/pkg/app/cli_test.go b/pkg/app/cli_test.go
index 255ff78..c599694 100644
--- a/pkg/app/cli_test.go
+++ b/pkg/app/cli_test.go
@@ -32,6 +32,42 @@ func TestCreateApp(t *testing.T) {
assert.NotNil(t, app.Before)
}
+func TestCreateAppTemplate_EmptyCommandName(t *testing.T) {
+ term := terminal.Color()
+ ctx := terminal.Context(context.Background(), term)
+
+ app := CreateAppTemplate(ctx, "", "Akamai CLI", "some description", version.Version)
+ assert.Equal(t, "akamai", app.Name)
+ assert.Equal(t, "Akamai CLI", app.Usage)
+ assert.Equal(t, "some description", app.Description)
+ assert.Equal(t, version.Version, app.Version)
+ assert.Equal(t, term, app.Writer)
+ assert.Equal(t, term.Error(), app.ErrWriter)
+ assert.Equal(t, "Copyright (C) Akamai Technologies, Inc", app.Copyright)
+ assert.True(t, app.EnableBashCompletion)
+ assert.True(t, hasFlag(app, "edgerc"))
+ assert.True(t, hasFlag(app, "section"))
+ assert.True(t, hasFlag(app, "accountkey"))
+}
+
+func TestCreateAppTemplate_NonEmptyCommandName(t *testing.T) {
+ term := terminal.Color()
+ ctx := terminal.Context(context.Background(), term)
+
+ app := CreateAppTemplate(ctx, "test", "Akamai CLI", "some description", version.Version)
+ assert.Equal(t, "akamai-test", app.Name)
+}
+
+func TestCreateAppTemplate_NonEmptyCommandNameWithEnvAKAMAI_CLI(t *testing.T) {
+ term := terminal.Color()
+ ctx := terminal.Context(context.Background(), term)
+
+ assert.NoError(t, os.Setenv("AKAMAI_CLI", ""))
+ app := CreateAppTemplate(ctx, "test", "", "", "")
+ assert.Equal(t, "akamai test", app.Name)
+ assert.NoError(t, os.Unsetenv("AKAMAI_CLI"))
+}
+
func TestVersion(t *testing.T) {
term := terminal.Color()
ctx := terminal.Context(context.Background(), term)
diff --git a/pkg/commands/command.go b/pkg/commands/command.go
index 12a8306..2ab717d 100644
--- a/pkg/commands/command.go
+++ b/pkg/commands/command.go
@@ -25,32 +25,44 @@ import (
"strings"
"syscall"
- "github.com/fatih/color"
- "github.com/urfave/cli/v2"
-
"github.com/akamai/cli/pkg/app"
"github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/tools"
+ "github.com/akamai/cli/pkg/version"
+ "github.com/fatih/color"
+ "github.com/urfave/cli/v2"
)
-type command struct {
- Name string `json:"name"`
- Aliases []string `json:"aliases"`
- Version string `json:"version"`
- Description string `json:"description"`
- Usage string `json:"usage"`
- Arguments string `json:"arguments"`
- Bin string `json:"bin"`
- AutoComplete bool `json:"auto-complete"`
-
- Flags []cli.Flag `json:"-"`
- Docs string `json:"-"`
- BinSuffix string `json:"-"`
- OS string `json:"-"`
- Arch string `json:"-"`
- Subcommands []*cli.Command `json:"-"`
-}
+type (
+ command struct {
+ Name string `json:"name"`
+ Aliases []string `json:"aliases"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ Usage string `json:"usage"`
+ Arguments string `json:"arguments"`
+ Bin string `json:"bin"`
+ AutoComplete bool `json:"auto-complete"`
+
+ Flags []cli.Flag `json:"-"`
+ Docs string `json:"-"`
+ BinSuffix string `json:"-"`
+ OS string `json:"-"`
+ Arch string `json:"-"`
+ Subcommands []*cli.Command `json:"-"`
+ }
+
+ // Command represents an external command being prepared or run
+ Command struct {
+ cmd *exec.Cmd
+ }
+
+ // Cmd is a wrapper for exec.Cmd methods
+ Cmd interface {
+ Run() error
+ }
+)
func getBuiltinCommands(c *cli.Context) []subcommands {
commands := make([]subcommands, 0)
@@ -107,13 +119,19 @@ func subcommandToCliCommands(from subcommands, gitRepo git.Repository, langManag
SkipFlagParsing: true,
BashComplete: func(c *cli.Context) {
if command.AutoComplete {
- executable, err := findExec(c.Context, langManager, c.Command.Name)
+ executable, packageReqs, err := findExec(c.Context, langManager, c.Command.Name)
+ if err != nil {
+ return
+ }
+
+ packageDir, err := findBinPackageDir(executable)
if err != nil {
return
}
executable = append(executable, os.Args[2:]...)
- if err = passthruCommand(executable); err != nil {
+ subCmd := createCommand(executable[0], executable[1:])
+ if err = passthruCommand(c.Context, subCmd, langManager, *packageReqs, packageDir); err != nil {
return
}
}
@@ -267,7 +285,8 @@ func createInstalledCommands(_ context.Context, gitRepo git.Repository, langMana
return commands
}
-func findExec(ctx context.Context, langManager packages.LangManager, cmd string) ([]string, error) {
+// findExec returns paths to language interpreter (if necessary) and package binary
+func findExec(ctx context.Context, langManager packages.LangManager, cmd string) ([]string, *packages.LanguageRequirements, error) {
// "command" becomes: akamai-command, and akamaiCommand
// "command-name" becomes: akamai-command-name, and akamaiCommandName
cmdName := "akamai"
@@ -280,7 +299,7 @@ func findExec(ctx context.Context, langManager packages.LangManager, cmd string)
systemPath := os.Getenv("PATH")
packagePaths := getPackageBinPaths()
if err := os.Setenv("PATH", packagePaths); err != nil {
- return nil, err
+ return nil, nil, err
}
// Quick look for executables on the path
@@ -292,16 +311,16 @@ func findExec(ctx context.Context, langManager packages.LangManager, cmd string)
if path != "" {
if err := os.Setenv("PATH", systemPath); err != nil {
- return nil, err
+ return nil, nil, err
}
- return []string{path}, nil
+ return []string{path}, nil, nil
}
if err := os.Setenv("PATH", systemPath); err != nil {
- return nil, err
+ return nil, nil, err
}
if packagePaths == "" {
- return nil, errors.New("no executables found")
+ return nil, nil, packages.ErrNoExeFound
}
for _, path := range filepath.SplitList(packagePaths) {
@@ -328,29 +347,28 @@ func findExec(ctx context.Context, langManager packages.LangManager, cmd string)
continue
}
- cmdFile := files[0]
+ cmdBinary := files[0]
- packageDir := findPackageDir(filepath.Dir(cmdFile))
+ packageDir := findPackageDir(filepath.Dir(cmdBinary))
cmdPackage, err := readPackage(packageDir)
if err != nil {
- return nil, err
+ return nil, nil, err
}
- cmd, err := langManager.FindExec(ctx, cmdPackage.Requirements, cmdFile)
+ comm, err := langManager.FindExec(ctx, cmdPackage.Requirements, cmdBinary)
if err != nil {
- return nil, err
+ return nil, nil, err
}
- return cmd, nil
+ return comm, &cmdPackage.Requirements, nil
}
- return nil, errors.New("no executables found")
+ return nil, nil, packages.ErrNoExeFound
}
func getPackageBinPaths() string {
path := ""
- akamaiCliPath, err := tools.GetAkamaiCliSrcPath()
- if err == nil && akamaiCliPath != "" {
+ if akamaiCliPath, err := tools.GetAkamaiCliSrcPath(); err == nil {
paths, _ := filepath.Glob(filepath.Join(akamaiCliPath, "*"))
if len(paths) > 0 {
path += strings.Join(paths, string(os.PathListSeparator))
@@ -364,11 +382,31 @@ func getPackageBinPaths() string {
return path
}
-func passthruCommand(executable []string) error {
- subCmd := exec.Command(executable[0], executable[1:]...)
- subCmd.Stdin = os.Stdin
- subCmd.Stderr = os.Stderr
- subCmd.Stdout = os.Stdout
+// passthruCommand performs the external Cmd invocation and previous set up, if required
+func passthruCommand(ctx context.Context, subCmd Cmd, langManager packages.LangManager, languageRequirements packages.LanguageRequirements, dirName string) error {
+ /*
+ Checking if any additional VE set up for Python commands is required.
+ Just run package setup if both conditions below are met:
+ * required python >= 3.0.0
+ * virtual environment is not present
+ */
+ if v3Comparison := version.Compare(languageRequirements.Python, "3.0.0"); languageRequirements.Python != "" && (v3Comparison == version.Greater || v3Comparison == version.Equals) {
+ vePath, err := tools.GetPkgVenvPath(filepath.Base(dirName))
+ if err != nil {
+ return err
+ }
+ veExists, err := langManager.FileExists(vePath)
+ if err != nil {
+ return err
+ }
+ if !veExists {
+ if err := langManager.PrepareExecution(ctx, languageRequirements, dirName); err != nil {
+ return err
+ }
+ }
+ }
+ defer langManager.FinishExecution(ctx, languageRequirements, dirName)
+
err := subCmd.Run()
exitCode := 1
@@ -382,3 +420,38 @@ func passthruCommand(executable []string) error {
}
return nil
}
+
+func findBinPackageDir(binPath []string) (string, error) {
+ if len(binPath) == 0 {
+ return "", packages.ErrPackageExecutableNotFound
+ }
+
+ absPath, err := filepath.Abs(binPath[len(binPath)-1])
+ if err != nil {
+ return "", err
+ }
+
+ info, err := os.Stat(absPath)
+ if err != nil {
+ return "", err
+ }
+ if info.IsDir() {
+ return "", errors.New("the specified binary is a directory")
+ }
+
+ return filepath.Dir(filepath.Dir(absPath)), nil
+}
+
+func createCommand(name string, args []string) *Command {
+ comm := &Command{cmd: exec.Command(name, args...)}
+ comm.cmd.Stdin = os.Stdin
+ comm.cmd.Stderr = os.Stderr
+ comm.cmd.Stdout = os.Stdout
+
+ return comm
+}
+
+// Run starts the command and waits for it to complete
+func (c *Command) Run() error {
+ return c.cmd.Run()
+}
diff --git a/pkg/commands/command_config.go b/pkg/commands/command_config.go
index 1755a22..bf1393f 100644
--- a/pkg/commands/command_config.go
+++ b/pkg/commands/command_config.go
@@ -16,14 +16,13 @@ package commands
import (
"fmt"
- "github.com/akamai/cli/pkg/log"
- "github.com/fatih/color"
"strings"
"time"
"github.com/akamai/cli/pkg/config"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/terminal"
-
+ "github.com/fatih/color"
"github.com/urfave/cli/v2"
)
@@ -34,7 +33,7 @@ func cmdConfigSet(c *cli.Context) (e error) {
logger.Debug("CONFIG SET START")
defer func() {
if e == nil {
- logger.Debugf("CONFIG SET FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("CONFIG SET FINISH: %v", time.Since(start))
} else {
logger.Errorf("CONFIG SET ERROR: %v", e.Error())
}
@@ -59,7 +58,7 @@ func cmdConfigGet(c *cli.Context) (e error) {
logger.Debug("CONFIG GET START")
defer func() {
if e == nil {
- logger.Debugf("CONFIG GET FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("CONFIG GET FINISH: %v", time.Since(start))
} else {
logger.Errorf("CONFIG GET ERROR: %v", e.Error())
}
@@ -82,7 +81,7 @@ func cmdConfigUnset(c *cli.Context) (e error) {
logger.Debug("CONFIG UNSET START")
defer func() {
if e == nil {
- logger.Debugf("CONFIG UNSET FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("CONFIG UNSET FINISH: %v", time.Since(start))
} else {
logger.Errorf("CONFIG UNSET ERROR: %v", e.Error())
}
@@ -107,7 +106,7 @@ func cmdConfigList(c *cli.Context) (e error) {
logger.Debug("CONFIG LIST START")
defer func() {
if e == nil {
- logger.Debugf("CONFIG LIST FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("CONFIG LIST FINISH: %v", time.Since(start))
} else {
logger.Errorf("CONFIG LIST ERROR: %v", e.Error())
}
diff --git a/pkg/commands/command_config_test.go b/pkg/commands/command_config_test.go
index 0ee217e..188e826 100644
--- a/pkg/commands/command_config_test.go
+++ b/pkg/commands/command_config_test.go
@@ -2,13 +2,14 @@ package commands
import (
"fmt"
+ "os"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/terminal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "os"
- "testing"
)
func TestCmdConfigSet(t *testing.T) {
@@ -41,7 +42,7 @@ func TestCmdConfigSet(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "config",
Subcommands: []*cli.Command{
@@ -93,7 +94,7 @@ func TestCmdConfigGet(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "config",
Subcommands: []*cli.Command{
@@ -153,7 +154,7 @@ func TestCmdConfigUnset(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "config",
Subcommands: []*cli.Command{
@@ -223,7 +224,7 @@ func TestCmdConfigList(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "config",
Subcommands: []*cli.Command{
diff --git a/pkg/commands/command_help.go b/pkg/commands/command_help.go
index a52e3cc..a876157 100644
--- a/pkg/commands/command_help.go
+++ b/pkg/commands/command_help.go
@@ -15,9 +15,9 @@
package commands
import (
- "github.com/akamai/cli/pkg/log"
"os"
+ "github.com/akamai/cli/pkg/log"
"github.com/urfave/cli/v2"
)
diff --git a/pkg/commands/command_help_test.go b/pkg/commands/command_help_test.go
index b81b980..f1fea44 100644
--- a/pkg/commands/command_help_test.go
+++ b/pkg/commands/command_help_test.go
@@ -2,15 +2,16 @@ package commands
import (
"bytes"
+ "os"
+ "regexp"
+ "testing"
+
"github.com/akamai/cli/pkg/app"
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/terminal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "os"
- "regexp"
- "testing"
)
func TestCmdHelp(t *testing.T) {
@@ -51,7 +52,7 @@ func TestCmdHelp(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
wr := bytes.Buffer{}
testApp, ctx := setupTestApp(test.cmd, m)
testApp.Commands = append(testApp.Commands, &cli.Command{
diff --git a/pkg/commands/command_install.go b/pkg/commands/command_install.go
index b533728..c7cf891 100644
--- a/pkg/commands/command_install.go
+++ b/pkg/commands/command_install.go
@@ -18,20 +18,19 @@ import (
"context"
"errors"
"fmt"
- "github.com/akamai/cli/pkg/log"
"os"
"path/filepath"
"strings"
"time"
- "github.com/fatih/color"
- "github.com/urfave/cli/v2"
-
"github.com/akamai/cli/pkg/git"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/stats"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
+ "github.com/fatih/color"
+ "github.com/urfave/cli/v2"
)
var (
@@ -46,7 +45,7 @@ func cmdInstall(git git.Repository, langManager packages.LangManager) cli.Action
logger.Debug("INSTALL START")
defer func() {
if e == nil {
- logger.Debugf("INSTALL FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("INSTALL FINISH: %v", time.Since(start))
} else {
var exitErr cli.ExitCoder
if errors.As(e, &exitErr) && exitErr.ExitCode() == 0 {
@@ -91,16 +90,12 @@ func packageListDiff(c *cli.Context, oldcmds []subcommands) {
var old []command
for _, oldcmd := range oldcmds {
- for _, cmd := range oldcmd.Commands {
- old = append(old, cmd)
- }
+ old = append(old, oldcmd.Commands...)
}
var newCmds []command
for _, newcmd := range cmds {
- for _, cmd := range newcmd.Commands {
- newCmds = append(newCmds, cmd)
- }
+ newCmds = append(newCmds, newcmd.Commands...)
}
var added = make(map[string]bool)
diff --git a/pkg/commands/command_install_test.go b/pkg/commands/command_install_test.go
index 3720ccc..39ad2fc 100644
--- a/pkg/commands/command_install_test.go
+++ b/pkg/commands/command_install_test.go
@@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
+ git2 "gopkg.in/src-d/go-git.v4"
)
func TestCmdInstall(t *testing.T) {
@@ -37,7 +38,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
})
m.term.On("OK").Return().Once()
m.term.On("Spinner").Return(m.term).Once()
@@ -70,7 +71,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
input, err := ioutil.ReadFile("./testdata/.akamai-cli/src/cli-test-cmd/cli.json")
require.NoError(t, err)
output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL"))
@@ -106,7 +107,7 @@ func TestCmdInstall(t *testing.T) {
require.NoError(t, os.RemoveAll("./testdata/.akamai-cli/src/cli-test-cmd"))
},
},
- "package already exists": {
+ "package directory already exists": {
args: []string{"installed"},
init: func(t *testing.T, m *mocked) {
m.term.On("Spinner").Return(m.term).Once()
@@ -130,10 +131,11 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(fmt.Errorf("oops")).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
})
m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once()
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
+ m.gitRepo.On("Clone", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(git2.ErrRepositoryAlreadyExists)
},
withError: "Unable to clone repository: oops",
},
@@ -145,7 +147,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-invalid-json",
"https://github.com/akamai/cli-test-invalid-json.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo_invalid_json/cli.json", "./testdata/.akamai-cli/src/cli-test-invalid-json")
+ mustCopyFile(t, "./testdata/repo_invalid_json/cli.json", "./testdata/.akamai-cli/src/cli-test-invalid-json")
})
m.term.On("Spinner").Return(m.term).Once()
m.term.On("OK").Return().Once()
@@ -171,7 +173,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
})
m.term.On("Spinner").Return(m.term).Once()
m.term.On("OK").Return().Once()
@@ -201,7 +203,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
input, err := ioutil.ReadFile("./testdata/.akamai-cli/src/cli-test-cmd/cli.json")
require.NoError(t, err)
output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL"))
@@ -240,7 +242,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
})
m.term.On("Spinner").Return(m.term).Once()
m.term.On("OK").Return().Once()
@@ -278,7 +280,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
input, err := ioutil.ReadFile("./testdata/.akamai-cli/src/cli-test-cmd/cli.json")
require.NoError(t, err)
output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL"))
@@ -321,7 +323,7 @@ func TestCmdInstall(t *testing.T) {
m.gitRepo.On("Clone", "testdata/.akamai-cli/src/cli-test-cmd",
"https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once().
Run(func(args mock.Arguments) {
- copyFile(t, "./testdata/repo_no_binary/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
+ mustCopyFile(t, "./testdata/repo_no_binary/cli.json", "./testdata/.akamai-cli/src/cli-test-cmd")
input, err := ioutil.ReadFile("./testdata/.akamai-cli/src/cli-test-cmd/cli.json")
require.NoError(t, err)
output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL"))
@@ -365,7 +367,7 @@ func TestCmdInstall(t *testing.T) {
defer srv.Close()
require.NoError(t, os.Setenv("REPOSITORY_URL", srv.URL))
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
- m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}, nil}
command := &cli.Command{
Name: "install",
Action: cmdInstall(m.gitRepo, m.langManager),
diff --git a/pkg/commands/command_list.go b/pkg/commands/command_list.go
index 7af99c1..468f877 100644
--- a/pkg/commands/command_list.go
+++ b/pkg/commands/command_list.go
@@ -16,15 +16,13 @@ package commands
import (
"fmt"
- "github.com/akamai/cli/pkg/log"
"time"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/terminal"
-
+ "github.com/akamai/cli/pkg/tools"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
-
- "github.com/akamai/cli/pkg/tools"
)
func cmdList(c *cli.Context) (e error) {
@@ -34,7 +32,7 @@ func cmdList(c *cli.Context) (e error) {
logger.Debug("LIST START")
defer func() {
if e == nil {
- logger.Debugf("LIST FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("LIST FINISH: %v", time.Since(start))
} else {
logger.Errorf("LIST ERROR: %v", e.Error())
}
diff --git a/pkg/commands/command_list_test.go b/pkg/commands/command_list_test.go
index 89e1341..c267a5b 100644
--- a/pkg/commands/command_list_test.go
+++ b/pkg/commands/command_list_test.go
@@ -2,6 +2,11 @@ package commands
import (
"fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
@@ -9,10 +14,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
)
func TestCmdListWithRemote(t *testing.T) {
@@ -65,7 +66,7 @@ func TestCmdListWithRemote(t *testing.T) {
require.NoError(t, os.Setenv("AKAMAI_CLI_PACKAGE_REPO", srv.URL))
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "list",
Flags: []cli.Flag{
diff --git a/pkg/commands/command_search.go b/pkg/commands/command_search.go
index 1f7caae..cad6595 100644
--- a/pkg/commands/command_search.go
+++ b/pkg/commands/command_search.go
@@ -18,7 +18,6 @@ import (
"context"
"encoding/json"
"fmt"
- "github.com/akamai/cli/pkg/log"
"io/ioutil"
"net/http"
"os"
@@ -26,12 +25,11 @@ import (
"strings"
"time"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/terminal"
-
+ "github.com/akamai/cli/pkg/tools"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
-
- "github.com/akamai/cli/pkg/tools"
)
type packageList struct {
@@ -62,7 +60,7 @@ func cmdSearch(c *cli.Context) (e error) {
logger.Debug("SEARCH START")
defer func() {
if e == nil {
- logger.Debugf("SEARCH FINISHED: %v", time.Now().Sub(start))
+ logger.Debugf("SEARCH FINISHED: %v", time.Since(start))
} else {
logger.Errorf("SEARCH ERROR: %v", e.Error())
}
diff --git a/pkg/commands/command_search_test.go b/pkg/commands/command_search_test.go
index ff68c43..90a1782 100644
--- a/pkg/commands/command_search_test.go
+++ b/pkg/commands/command_search_test.go
@@ -2,6 +2,12 @@ package commands
import (
"fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
@@ -9,11 +15,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
)
func TestCmdSearch(t *testing.T) {
@@ -106,7 +107,7 @@ func TestCmdSearch(t *testing.T) {
require.NoError(t, os.Setenv("AKAMAI_CLI_PACKAGE_REPO", srv.URL))
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
t.Run(name, func(t *testing.T) {
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "search",
Action: cmdSearch,
diff --git a/pkg/commands/command_subcommand.go b/pkg/commands/command_subcommand.go
index aa7b2ff..b41ab76 100644
--- a/pkg/commands/command_subcommand.go
+++ b/pkg/commands/command_subcommand.go
@@ -21,16 +21,13 @@ import (
"runtime"
"strings"
+ "github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/packages"
-
"github.com/akamai/cli/pkg/stats"
"github.com/akamai/cli/pkg/terminal"
-
"github.com/fatih/color"
"github.com/urfave/cli/v2"
-
- "github.com/akamai/cli/pkg/git"
)
func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.ActionFunc {
@@ -41,7 +38,7 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act
commandName := strings.ToLower(c.Command.Name)
- executable, err := findExec(c.Context, langManager, commandName)
+ executable, _, err := findExec(c.Context, langManager, commandName)
if err != nil {
errMsg := color.RedString("Executable \"%s\" not found.", commandName)
logger.Error(errMsg)
@@ -58,7 +55,20 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act
cmdPackage, _ := readPackage(packageDir)
if cmdPackage.Requirements.Python != "" {
- var err error
+ exec, err := langManager.FindExec(c.Context, cmdPackage.Requirements, packageDir)
+ if err != nil {
+ return err
+ }
+
+ if len(executable) == 1 {
+ executable = append([]string{exec[0]}, executable...)
+ } else {
+ if strings.Contains(strings.ToLower(executable[0]), "python") ||
+ strings.Contains(strings.ToLower(executable[0]), "py.exe") {
+ executable[0] = exec[0]
+ }
+ }
+
if runtime.GOOS == "linux" {
_, err = os.Stat(filepath.Join(packageDir, ".local"))
} else if runtime.GOOS == "darwin" {
@@ -105,26 +115,54 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act
}
}
- executable = append(executable, c.Args().Slice()...)
if err := os.Setenv("AKAMAI_CLI_COMMAND", commandName); err != nil {
return err
}
if err := os.Setenv("AKAMAI_CLI_COMMAND_VERSION", currentCmd.Version); err != nil {
return err
}
+
+ cmdPackage, err = readPackage(packageDir)
+ if err != nil {
+ return err
+ }
+
stats.TrackEvent(c.Context, "exec", commandName, currentCmd.Version)
- executable = findAndAppendFlags(c, executable, "edgerc", "section")
- return passthruCommand(executable)
+
+ executable = prepareCommand(c, executable, c.Args().Slice(), "edgerc", "section", "accountkey")
+
+ subCmd := createCommand(executable[0], executable[1:])
+ return passthruCommand(c.Context, subCmd, langManager, cmdPackage.Requirements, fmt.Sprintf("cli-%s", cmdPackage.Commands[0].Name))
}
}
-func findAndAppendFlags(c *cli.Context, target []string, flags ...string) []string {
+func prepareCommand(c *cli.Context, command, args []string, flags ...string) []string {
+ // dont search for flags is there are no args
+ if len(args) == 0 {
+ return command
+ }
+ additionalFlags := findFlags(c, args, flags...)
+
+ if len(command) > 1 {
+ // for python or js append flags to the end
+ command = append(command, args...)
+ command = append(command, additionalFlags...)
+ } else {
+ command = append(command, additionalFlags...)
+ command = append(command, args...)
+ }
+
+ return command
+}
+
+func findFlags(c *cli.Context, target []string, flags ...string) []string {
+ var ret []string
for _, flagName := range flags {
if flagVal := c.String(flagName); flagVal != "" && !containsString(target, fmt.Sprintf("--%s", flagName)) {
- target = append(target, fmt.Sprintf("--%s", flagName), flagVal)
+ ret = append(ret, fmt.Sprintf("--%s", flagName), flagVal)
}
}
- return target
+ return ret
}
func containsString(s []string, item string) bool {
diff --git a/pkg/commands/command_subcommand_test.go b/pkg/commands/command_subcommand_test.go
index 5a75430..9f67f85 100644
--- a/pkg/commands/command_subcommand_test.go
+++ b/pkg/commands/command_subcommand_test.go
@@ -3,6 +3,7 @@ package commands
import (
"flag"
"os"
+ "os/exec"
"testing"
"github.com/akamai/cli/pkg/config"
@@ -28,6 +29,8 @@ func TestCmdSubcommand(t *testing.T) {
args: []string{"abc"},
init: func(t *testing.T, m *mocked) {
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
+ 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()
},
},
"run installed akamai echo command as binary with edgerc location": {
@@ -35,6 +38,8 @@ func TestCmdSubcommand(t *testing.T) {
args: []string{"abc"},
init: func(t *testing.T, m *mocked) {
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
+ 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()
},
},
"run installed akamai echo command as binary with alias": {
@@ -44,13 +49,8 @@ func TestCmdSubcommand(t *testing.T) {
section: "some_section",
init: func(t *testing.T, m *mocked) {
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
- },
- },
- "run installed akamai echo command with python required": {
- command: "echo-python",
- args: []string{"abc"},
- init: func(t *testing.T, m *mocked) {
- m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
+ 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()
},
},
"run installed akamai echo command as .cmd file": {
@@ -60,6 +60,8 @@ func TestCmdSubcommand(t *testing.T) {
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, "testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-cmd.cmd").
Return([]string{"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()
},
},
"run installed python akamai echo command": {
@@ -69,6 +71,8 @@ func TestCmdSubcommand(t *testing.T) {
m.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, "testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-cmd.cmd").
Return([]string{"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()
},
},
"executable not found": {
@@ -82,7 +86,7 @@ func TestCmdSubcommand(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
- m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}, nil}
command := &cli.Command{
Name: test.command,
Action: cmdSubcommand(m.gitRepo, m.langManager),
@@ -112,38 +116,167 @@ func TestCmdSubcommand(t *testing.T) {
}
}
-func TestFindAndAppendFlag(t *testing.T) {
+func TestPythonCmdSubcommand(t *testing.T) {
+ // run installed akamai echo command with python required
+ t.Run(
+ "run installed akamai echo command with python required",
+ func(t *testing.T) {
+ require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}, nil}
+ command := &cli.Command{
+ Name: "echo-python",
+ Action: cmdSubcommand(m.gitRepo, m.langManager),
+ }
+ app, ctx := setupTestApp(command, m)
+ args := os.Args[0:1]
+ args = append(args, "echo-python")
+ args = append(args, "abc")
+
+ // Using the system python avoids the need of shipping a python
+ // interpreter together with the test data. This wouldn't be a
+ // good option because of different OSes and CPU architectures.
+ if pythonBin, err := exec.LookPath("python"); err != nil {
+ // 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.cfg.On("GetValue", "cli", "enable-cli-statistics").Return("false", true)
+ 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"}, "testdata/.akamai-cli/src/cli-echo-python").
+ Return([]string{pythonBin}, nil).Once()
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(true, nil)
+ }
+
+ err := app.RunContext(ctx, args)
+
+ m.cfg.AssertExpectations(t)
+ require.NoError(t, err)
+ })
+}
+
+func TestPrepareCommand(t *testing.T) {
tests := map[string]struct {
- flagsInCtx map[string]string
- givenSlice []string
- givenFlags []string
- expected []string
+ flagsInCtx map[string]string
+ givenCommand []string
+ givenArgs []string
+ givenFlags []string
+ expected []string
}{
- "flags found on context and not in slice": {
+ "no flags and no args": {
+ givenCommand: []string{"command"},
+ expected: []string{"command"},
+ },
+ "no flags and with args": {
+ givenCommand: []string{"command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ expected: []string{"command", "--flag_1", "existing_value"},
+ },
+ "flags and no args": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"command"},
+ givenFlags: []string{"flag_2"},
+ expected: []string{"command"},
+ },
+ "flags and args not overlaping": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ givenFlags: []string{"flag_2"},
+ expected: []string{"command", "--flag_2", "other_value", "--flag_1", "existing_value"},
+ },
+ "flags found in context but not in args": {
flagsInCtx: map[string]string{
"flag_1": "some_value",
"flag_2": "other_value",
"flag_3": "abc",
},
- givenSlice: []string{"some", "command"},
- givenFlags: []string{"flag_1", "flag_3"},
- expected: []string{"some", "command", "--flag_1", "some_value", "--flag_3", "abc"},
+ givenCommand: []string{"command"},
+ givenFlags: []string{"flag_1", "flag_3"},
+ expected: []string{"command"},
},
- "flag found on context and in slice": {
+ "flag found in context and in args": {
flagsInCtx: map[string]string{
"flag_1": "some_value",
"flag_2": "other_value",
"flag_3": "abc",
},
- givenSlice: []string{"some", "command", "--flag_1", "existing_value"},
- givenFlags: []string{"flag_1"},
- expected: []string{"some", "command", "--flag_1", "existing_value"},
+ givenCommand: []string{"command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ givenFlags: []string{"flag_1"},
+ expected: []string{"command", "--flag_1", "existing_value"},
},
"flag does not have value": {
- flagsInCtx: map[string]string{},
- givenSlice: []string{"some", "command"},
- givenFlags: []string{"flag_1"},
- expected: []string{"some", "command"},
+ flagsInCtx: map[string]string{},
+ givenCommand: []string{"command"},
+ givenFlags: []string{"flag_1"},
+ expected: []string{"command"},
+ },
+
+ // tests for script commands like python or js
+ "script - no flags and no args": {
+ givenCommand: []string{"some", "command"},
+ expected: []string{"some", "command"},
+ },
+ "script - no flags and with args": {
+ givenCommand: []string{"some", "command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ expected: []string{"some", "command", "--flag_1", "existing_value"},
+ },
+ "script - flags and no args": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"some", "command"},
+ givenFlags: []string{"flag_2"},
+ expected: []string{"some", "command"},
+ },
+ "script - flags and args not overlaping": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"some", "command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ givenFlags: []string{"flag_2"},
+ expected: []string{"some", "command", "--flag_1", "existing_value", "--flag_2", "other_value"},
+ },
+ "script - flags found in context but not in args": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"some", "command"},
+ givenFlags: []string{"flag_1", "flag_3"},
+ expected: []string{"some", "command"},
+ },
+ "script - flag found in context and in args": {
+ flagsInCtx: map[string]string{
+ "flag_1": "some_value",
+ "flag_2": "other_value",
+ "flag_3": "abc",
+ },
+ givenCommand: []string{"some", "command"},
+ givenArgs: []string{"--flag_1", "existing_value"},
+ givenFlags: []string{"flag_1"},
+ expected: []string{"some", "command", "--flag_1", "existing_value"},
+ },
+ "script - flag does not have value": {
+ flagsInCtx: map[string]string{},
+ givenCommand: []string{"some", "command"},
+ givenFlags: []string{"flag_1"},
+ expected: []string{"some", "command"},
},
}
@@ -157,7 +290,7 @@ func TestFindAndAppendFlag(t *testing.T) {
for name, value := range test.flagsInCtx {
require.NoError(t, c.Set(name, value))
}
- res := findAndAppendFlags(c, test.givenSlice, test.givenFlags...)
+ res := prepareCommand(c, test.givenCommand, test.givenArgs, test.givenFlags...)
assert.Equal(t, test.expected, res)
})
}
diff --git a/pkg/commands/command_test.go b/pkg/commands/command_test.go
index e9a18f1..19ff7f4 100644
--- a/pkg/commands/command_test.go
+++ b/pkg/commands/command_test.go
@@ -2,8 +2,11 @@ package commands
import (
"context"
+ "errors"
"fmt"
+ "io/fs"
"os"
+ "os/exec"
"strings"
"testing"
@@ -38,3 +41,116 @@ func TestSubcommandsToCliCommands_packagePrefix(t *testing.T) {
assert.True(t, strings.HasPrefix(cmd.Aliases[0], fmt.Sprintf("%s/", from.Pkg)), "there should be an alias with the package prefix")
}
}
+
+func TestPassthruCommand(t *testing.T) {
+ tests := map[string]struct {
+ executable []string
+ init func(*mocked)
+ langRequirements packages.LanguageRequirements
+ dirName string
+ withError error
+ }{
+ "golang binary": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo"},
+ init: func(m *mocked) {
+ m.langManager.On(
+ "FinishExecution", packages.LanguageRequirements{Go: "1.15.0"},
+ "./testdata/.akamai-cli/src/cli-echo/").Once()
+ m.cmd.On("Run").Return(nil).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Go: "1.15.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo/",
+ },
+ "python 2": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FinishExecution", packages.LanguageRequirements{Python: "2.7.10"}, "./testdata/.akamai-cli/src/cli-echo-python/").Once()
+ m.cmd.On("Run").Return(nil).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Python: "2.7.10"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ },
+ "python 3, ve exists": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(true, nil)
+ m.langManager.On("FinishExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Once()
+ m.cmd.On("Run").Return(nil).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Python: "3.0.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ },
+ "python 3, ve does not exist": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(false, nil)
+ m.langManager.On("PrepareExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Return(nil).Once()
+ m.langManager.On("FinishExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Once()
+ m.cmd.On("Run").Return(nil).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Python: "3.0.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ },
+ "python 3, ve does not exist - error running the external command": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(false, nil)
+ m.langManager.On("PrepareExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Return(nil).Once()
+ m.langManager.On("FinishExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Return().Once()
+ m.cmd.On("Run").Return(&exec.ExitError{ProcessState: &os.ProcessState{}}).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Python: "3.0.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ withError: fmt.Errorf("wanted"),
+ },
+ "python 3, ve does not exist - error preparing execution": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(false, nil)
+ m.langManager.On("PrepareExecution", packages.LanguageRequirements{Python: "3.0.0"}, "./testdata/.akamai-cli/src/cli-echo-python/").Return(packages.ErrPackageManagerExec).Once()
+ },
+ langRequirements: packages.LanguageRequirements{Python: "3.0.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ withError: packages.ErrPackageManagerExec,
+ },
+ "python 3 - fs permission error to read VE": {
+ executable: []string{"./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-python"},
+ init: func(m *mocked) {
+ m.langManager.On("FileExists", "testdata/.akamai-cli/venv/cli-echo-python").Return(false, fs.ErrPermission)
+ },
+ langRequirements: packages.LanguageRequirements{Python: "3.0.0"},
+ dirName: "./testdata/.akamai-cli/src/cli-echo-python/",
+ withError: fs.ErrPermission,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cliHome, cliHomeOK := os.LookupEnv("AKAMAI_CLI_HOME")
+ if err := os.Setenv("AKAMAI_CLI_HOME", "testdata"); err != nil {
+ return
+ }
+ defer func() {
+ if cliHomeOK {
+ _ = os.Setenv("AKAMAI_CLI_HOME", cliHome)
+ } else {
+ _ = os.Unsetenv("AKAMAI_CLI_HOME")
+ }
+ }()
+
+ m := &mocked{langManager: &packages.Mock{}, cmd: &MockCmd{}}
+ test.init(m)
+
+ err := passthruCommand(context.Background(), m.cmd, m.langManager, test.langRequirements, test.dirName)
+
+ m.cmd.AssertExpectations(t)
+ m.langManager.AssertExpectations(t)
+
+ if test.withError != nil {
+ assert.True(t, errors.As(err, &test.withError), "want: '%s'; got '%s'", test.withError.Error(), err.Error())
+ return
+ }
+ assert.NoError(t, err)
+ })
+ }
+}
diff --git a/pkg/commands/command_uninstall.go b/pkg/commands/command_uninstall.go
index 5129525..6f96c36 100644
--- a/pkg/commands/command_uninstall.go
+++ b/pkg/commands/command_uninstall.go
@@ -17,16 +17,15 @@ package commands
import (
"context"
"fmt"
- "github.com/akamai/cli/pkg/log"
- "github.com/akamai/cli/pkg/packages"
"os"
"path/filepath"
"time"
+ "github.com/akamai/cli/pkg/log"
+ "github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/stats"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
-
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
@@ -39,7 +38,7 @@ func cmdUninstall(langManager packages.LangManager) cli.ActionFunc {
logger.Debug("UNINSTALL START")
defer func() {
if e == nil {
- logger.Debugf("UNINSTALL FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("UNINSTALL FINISH: %v", time.Since(start))
} else {
logger.Errorf("UNINSTALL ERROR: %v", e.Error())
}
@@ -60,7 +59,7 @@ func cmdUninstall(langManager packages.LangManager) cli.ActionFunc {
func uninstallPackage(ctx context.Context, langManager packages.LangManager, cmd string, logger log.Logger) error {
term := terminal.Get(ctx)
- exec, err := findExec(ctx, langManager, cmd)
+ exec, _, err := findExec(ctx, langManager, cmd)
if err != nil {
return fmt.Errorf("command \"%s\" not found. Try \"%s help\"", cmd, tools.Self())
}
@@ -87,6 +86,19 @@ func uninstallPackage(ctx context.Context, langManager packages.LangManager, cmd
return fmt.Errorf("unable to remove directory: %s", repoDir)
}
+ venvPath, err := tools.GetPkgVenvPath(fmt.Sprintf("cli-%s", cmd))
+ if err != nil {
+ return err
+ }
+ if _, err := os.Stat(venvPath); err == nil || !os.IsNotExist(err) {
+ logger.Debugf("Attempting to remove package virtualenv directory")
+ if err := os.RemoveAll(venvPath); err != nil {
+ term.Spinner().Fail()
+ logger.Errorf("unable to remove virtualenv directory: %s", venvPath)
+ return fmt.Errorf("unable to remove virtualenv directory: %s", repoDir)
+ }
+ }
+
term.Spinner().OK()
return nil
diff --git a/pkg/commands/command_uninstall_test.go b/pkg/commands/command_uninstall_test.go
index 08519af..776ba7a 100644
--- a/pkg/commands/command_uninstall_test.go
+++ b/pkg/commands/command_uninstall_test.go
@@ -2,6 +2,9 @@ package commands
import (
"fmt"
+ "os"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/packages"
@@ -11,8 +14,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "os"
- "testing"
)
func TestCmdUninstall(t *testing.T) {
@@ -24,8 +25,8 @@ func TestCmdUninstall(t *testing.T) {
"uninstall command": {
args: []string{"echo-uninstall"},
init: func(t *testing.T, m *mocked) {
- copyFile(t, "./testdata/.akamai-cli/src/cli-echo/cli.json", "./testdata/.akamai-cli/src/cli-echo-uninstall")
- copyFile(t, "./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin")
+ mustCopyFile(t, "./testdata/.akamai-cli/src/cli-echo/cli.json", "./testdata/.akamai-cli/src/cli-echo-uninstall")
+ mustCopyFile(t, "./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin")
err := os.Rename("./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo-uninstall")
require.NoError(t, err)
err = os.Chmod("./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo-uninstall", 0755)
@@ -41,7 +42,7 @@ func TestCmdUninstall(t *testing.T) {
"package does not contain cli.json": {
args: []string{"echo-uninstall"},
init: func(t *testing.T, m *mocked) {
- copyFile(t, "./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin")
+ mustCopyFile(t, "./testdata/.akamai-cli/src/cli-echo/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin")
err := os.Rename("./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo", "./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo-uninstall")
require.NoError(t, err)
err = os.Chmod("./testdata/.akamai-cli/src/cli-echo-uninstall/bin/akamai-echo-uninstall", 0755)
@@ -67,7 +68,7 @@ func TestCmdUninstall(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
- m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}, nil}
command := &cli.Command{
Name: "uninstall",
Action: cmdUninstall(m.langManager),
diff --git a/pkg/commands/command_update.go b/pkg/commands/command_update.go
index 33a7b48..a5dbc54 100644
--- a/pkg/commands/command_update.go
+++ b/pkg/commands/command_update.go
@@ -17,18 +17,17 @@ package commands
import (
"context"
"fmt"
- "github.com/akamai/cli/pkg/packages"
"path/filepath"
"strings"
"time"
- "github.com/fatih/color"
- "github.com/urfave/cli/v2"
-
"github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/log"
+ "github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
+ "github.com/fatih/color"
+ "github.com/urfave/cli/v2"
)
func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.ActionFunc {
@@ -39,7 +38,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act
logger.Debug("UPDATE START")
defer func() {
if e == nil {
- logger.Debugf("UPDATE FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("UPDATE FINISH: %v", time.Since(start))
} else {
logger.Errorf("UPDATE ERROR: %v", e.Error())
}
@@ -75,7 +74,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act
func updatePackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, logger log.Logger, cmd string, forceBinary bool) error {
term := terminal.Get(ctx)
- exec, err := findExec(ctx, langManager, cmd)
+ exec, _, err := findExec(ctx, langManager, cmd)
if err != nil {
return cli.Exit(color.RedString("Command \"%s\" not found. Try \"%s help\".\n", cmd, tools.Self()), 1)
}
diff --git a/pkg/commands/command_update_test.go b/pkg/commands/command_update_test.go
index f1e2495..1fe17ac 100644
--- a/pkg/commands/command_update_test.go
+++ b/pkg/commands/command_update_test.go
@@ -2,6 +2,11 @@ package commands
import (
"fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/packages"
@@ -15,10 +20,6 @@ import (
gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
)
func TestCmdUpdate(t *testing.T) {
@@ -234,7 +235,7 @@ func TestCmdUpdate(t *testing.T) {
}))
defer srv.Close()
require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", "./testdata"))
- m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.Mock{}, &packages.Mock{}, nil}
command := &cli.Command{
Name: "update",
Action: cmdUpdate(m.gitRepo, m.langManager),
diff --git a/pkg/commands/command_upgrade.go b/pkg/commands/command_upgrade.go
index 30634f3..c9e974b 100644
--- a/pkg/commands/command_upgrade.go
+++ b/pkg/commands/command_upgrade.go
@@ -15,14 +15,13 @@
package commands
import (
- "github.com/akamai/cli/pkg/log"
"os"
"time"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/stats"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/version"
-
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
@@ -33,7 +32,7 @@ func cmdUpgrade(c *cli.Context) error {
start := time.Now()
logger.Debug("UPGRADE START")
defer func() {
- logger.Debugf("UPGRADE FINISH: %v", time.Now().Sub(start))
+ logger.Debugf("UPGRADE FINISH: %v", time.Since(start))
}()
term := terminal.Get(c.Context)
diff --git a/pkg/commands/command_upgrade_test.go b/pkg/commands/command_upgrade_test.go
index f6b96b3..3aa31ee 100644
--- a/pkg/commands/command_upgrade_test.go
+++ b/pkg/commands/command_upgrade_test.go
@@ -138,7 +138,7 @@ func TestCmdUpgrade(t *testing.T) {
}
}))
require.NoError(t, os.Setenv("CLI_REPOSITORY", srv.URL))
- m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil}
+ m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil}
command := &cli.Command{
Name: "upgrade",
Action: cmdUpgrade,
diff --git a/pkg/commands/constants.go b/pkg/commands/constants.go
index 34d8dda..e1ff0e2 100644
--- a/pkg/commands/constants.go
+++ b/pkg/commands/constants.go
@@ -6,6 +6,5 @@ import (
const (
alreadyUptoDate = "already up-to-date"
- objectNotFound = "object not found"
- sleepTime24Hours = time.Hour * 24
+ sleep24HDuration = time.Hour * 24
)
diff --git a/pkg/commands/helpers_test.go b/pkg/commands/helpers_test.go
index 8b553d5..1544145 100644
--- a/pkg/commands/helpers_test.go
+++ b/pkg/commands/helpers_test.go
@@ -2,23 +2,85 @@ package commands
import (
"context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/git"
"github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/terminal"
+ "github.com/apex/log"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
- "io"
- "os"
- "path/filepath"
- "testing"
)
+var testFiles = map[string][]string{
+ "cli-echo": {"akamai-e", "akamai-echo", "akamai-echo-cmd.cmd"},
+ "cli-echo-invalid-json": {"akamai-echo-invalid-json"},
+}
+
+// TestMain prepares test binary used as cli command in tests and copies it to each directory specified in 'testFiles' variable
+// The reason why binaries are not included in the repository is to make the tests pass on different operating systems
+// After tests are executed, all generated binaries are removed
+func TestMain(m *testing.M) {
+ binaryPath, err := buildTestBinary()
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+
+ for dir, files := range testFiles {
+ for _, file := range files {
+ targetDir := filepath.Join("testdata", ".akamai-cli", "src", dir, "bin")
+ if err := copyFile(binaryPath, targetDir); err != nil {
+ log.Fatal(err.Error())
+ }
+ if err := os.Rename(filepath.Join(targetDir, filepath.Base(binaryPath)), filepath.Join(targetDir, file)); err != nil {
+ log.Fatal(err.Error())
+ }
+ if err := os.Chmod(filepath.Join(targetDir, file), 0755); err != nil {
+ log.Fatal(err.Error())
+ }
+ }
+ }
+ exitCode := m.Run()
+ if err := os.RemoveAll(binaryPath); err != nil {
+ log.Fatal(err.Error())
+ }
+ for dir := range testFiles {
+ targetDir := filepath.Join("testdata", ".akamai-cli", "src", dir, "bin")
+ if err := os.RemoveAll(targetDir); err != nil {
+ log.Fatal(err.Error())
+ }
+ }
+ os.Exit(exitCode)
+}
+
+func buildTestBinary() (string, error) {
+ bin, err := exec.LookPath("go")
+ if err != nil {
+ return "", err
+ }
+ sourcePath := filepath.Join("testdata", "example-binary.go")
+ targetPath := filepath.Join("testdata", "example-binary")
+ if runtime.GOOS == "windows" {
+ targetPath = fmt.Sprintf("%s.exe", targetPath)
+ }
+ cmd := exec.Command(bin, "build", "-o", targetPath, "-ldflags", "-s -w", sourcePath)
+ _, err = cmd.Output()
+ return targetPath, err
+}
+
type mocked struct {
term *terminal.Mock
cfg *config.Mock
gitRepo *git.Mock
langManager *packages.Mock
+ cmd *MockCmd
}
func setupTestApp(command *cli.Command, m *mocked) (*cli.App, context.Context) {
@@ -42,21 +104,35 @@ func setupTestApp(command *cli.Command, m *mocked) (*cli.App, context.Context) {
return app, ctx
}
-func copyFile(t *testing.T, src, dst string) {
+func copyFile(src, dst string) error {
err := os.MkdirAll(dst, 0755)
- require.NoError(t, err)
+ if err != nil {
+ return err
+ }
srcFile, err := os.Open(src)
- require.NoError(t, err)
+ if err != nil {
+ return err
+ }
defer func() {
- require.NoError(t, srcFile.Close())
+ if err := srcFile.Close(); err != nil {
+ log.Error(err.Error())
+ }
}()
destFile, err := os.Create(filepath.Join(dst, filepath.Base(srcFile.Name())))
- require.NoError(t, err)
+ if err != nil {
+ return err
+ }
defer func() {
- require.NoError(t, destFile.Close())
+ if err := destFile.Close(); err != nil {
+ log.Error(err.Error())
+ }
}()
- _, err = io.Copy(destFile, srcFile)
- require.NoError(t, err)
- err = destFile.Sync()
- require.NoError(t, err)
+ if _, err := io.Copy(destFile, srcFile); err != nil {
+ return err
+ }
+ return destFile.Sync()
+}
+
+func mustCopyFile(t *testing.T, src, dst string) {
+ require.NoError(t, copyFile(src, dst))
}
diff --git a/pkg/commands/mock.go b/pkg/commands/mock.go
new file mode 100644
index 0000000..6f81d0a
--- /dev/null
+++ b/pkg/commands/mock.go
@@ -0,0 +1,19 @@
+package commands
+
+import "github.com/stretchr/testify/mock"
+
+// MockCmd is used to track activity on command.Command
+type MockCmd struct {
+ mock.Mock
+}
+
+// String mimics the behavior or (*Command) String()
+func (c *MockCmd) String() string {
+ return "MockCmd"
+}
+
+// Run mimics the behavior of (*Command) Run()
+func (c *MockCmd) Run() error {
+ args := c.Called()
+ return args.Error(0)
+}
diff --git a/pkg/commands/subcommands.go b/pkg/commands/subcommands.go
index 4f802a6..5c269fb 100644
--- a/pkg/commands/subcommands.go
+++ b/pkg/commands/subcommands.go
@@ -28,12 +28,10 @@ import (
"strings"
"text/template"
- "github.com/akamai/cli/pkg/packages"
-
"github.com/akamai/cli/pkg/log"
- "github.com/urfave/cli/v2"
-
+ "github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/tools"
+ "github.com/urfave/cli/v2"
)
type subcommands struct {
@@ -98,6 +96,7 @@ func findPackageDir(dir string) string {
}
}
+ // at this point, dir points to the package directory, with cli.json
return dir
}
diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo-invalid-json/bin/akamai-echo-invalid-json b/pkg/commands/testdata/.akamai-cli/src/cli-echo-invalid-json/bin/akamai-echo-invalid-json
deleted file mode 100755
index 1c3df78..0000000
Binary files a/pkg/commands/testdata/.akamai-cli/src/cli-echo-invalid-json/bin/akamai-echo-invalid-json and /dev/null differ
diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/bin/akamai-echo-python b/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/bin/akamai-echo-python
index 1c3df78..bfe214e 100755
Binary files a/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/bin/akamai-echo-python and b/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/bin/akamai-echo-python differ
diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-e b/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-e
deleted file mode 100755
index 1c3df78..0000000
Binary files a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-e and /dev/null differ
diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo b/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo
deleted file mode 100755
index 1c3df78..0000000
Binary files a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo and /dev/null differ
diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-cmd.cmd b/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-cmd.cmd
deleted file mode 100755
index 1c3df78..0000000
Binary files a/pkg/commands/testdata/.akamai-cli/src/cli-echo/bin/akamai-echo-cmd.cmd and /dev/null differ
diff --git a/pkg/commands/testdata/example-binary.go b/pkg/commands/testdata/example-binary.go
new file mode 100644
index 0000000..e0b87a2
--- /dev/null
+++ b/pkg/commands/testdata/example-binary.go
@@ -0,0 +1,5 @@
+package main
+
+func main() {
+ return
+}
diff --git a/pkg/commands/upgrade.go b/pkg/commands/upgrade.go
index ba62f19..41690d1 100644
--- a/pkg/commands/upgrade.go
+++ b/pkg/commands/upgrade.go
@@ -1,4 +1,5 @@
-//+build !noautoupgrade
+//go:build !noautoupgrade
+// +build !noautoupgrade
// Copyright 2018. Akamai Technologies, Inc
//
@@ -30,14 +31,14 @@ import (
"text/template"
"time"
- "github.com/akamai/cli/pkg/log"
- "github.com/urfave/cli/v2"
-
"github.com/akamai/cli/pkg/config"
+ "github.com/akamai/cli/pkg/log"
+ "github.com/akamai/cli/pkg/packages"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/version"
"github.com/fatih/color"
"github.com/inconshreveable/go-update"
+ "github.com/urfave/cli/v2"
)
// CheckUpgradeVersion ...
@@ -70,7 +71,7 @@ func CheckUpgradeVersion(ctx context.Context, force bool) string {
}
currentTime := time.Now()
- if lastUpgrade.Add(sleepTime24Hours).Before(currentTime) {
+ if lastUpgrade.Add(sleep24HDuration).Before(currentTime) {
checkForUpgrade = true
}
}
@@ -84,7 +85,7 @@ func CheckUpgradeVersion(ctx context.Context, force bool) string {
latestVersion := getLatestReleaseVersion(ctx)
comp := version.Compare(version.Version, latestVersion)
- if comp == 1 {
+ if comp == version.Smaller {
term.Spinner().Stop(terminal.SpinnerStatusOK)
_, _ = term.Writeln("You can find more details about the new version here: https://github.com/akamai/cli/releases")
if answer, err := term.Confirm(fmt.Sprintf(
@@ -96,7 +97,7 @@ func CheckUpgradeVersion(ctx context.Context, force bool) string {
}
return latestVersion
}
- if comp == 0 {
+ if comp == version.Equals {
return version.Version
}
}
@@ -230,8 +231,9 @@ func UpgradeCli(ctx context.Context, latestVersion string) bool {
if err == nil {
os.Args[0] = selfPath
}
- err = passthruCommand(os.Args)
- if err != nil {
+
+ subCmd := createCommand(os.Args[0], os.Args[1:])
+ if err = passthruCommand(ctx, subCmd, packages.NewLangManager(), packages.LanguageRequirements{}, selfPath); err != nil {
cli.OsExiter(1)
return false
}
diff --git a/pkg/commands/upgrade_noop.go b/pkg/commands/upgrade_noop.go
index 96a6d66..66947b8 100644
--- a/pkg/commands/upgrade_noop.go
+++ b/pkg/commands/upgrade_noop.go
@@ -1,4 +1,5 @@
-//+build noautoupgrade
+//go:build noautoupgrade
+// +build noautoupgrade
// Copyright 2018. Akamai Technologies, Inc
//
@@ -18,6 +19,7 @@ package commands
import (
"context"
+
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7a5d8b8..429ccb9 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -17,17 +17,16 @@ package config
import (
"context"
"errors"
- "github.com/akamai/cli/pkg/log"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
- "github.com/go-ini/ini"
-
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/tools"
+ "github.com/go-ini/ini"
)
const (
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index da6717d..4e05108 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -2,14 +2,15 @@ package config
import (
"context"
- "github.com/akamai/cli/pkg/terminal"
- "github.com/go-ini/ini"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"io/ioutil"
"os"
"path/filepath"
"testing"
+
+ "github.com/akamai/cli/pkg/terminal"
+ "github.com/go-ini/ini"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestNewIni(t *testing.T) {
diff --git a/pkg/config/mock.go b/pkg/config/mock.go
index 7825d95..4c229be 100644
--- a/pkg/config/mock.go
+++ b/pkg/config/mock.go
@@ -2,6 +2,7 @@ package config
import (
"context"
+
"github.com/stretchr/testify/mock"
)
@@ -11,7 +12,7 @@ type Mock struct {
}
// Save mock
-func (m *Mock) Save(ctx context.Context) error {
+func (m *Mock) Save(_ context.Context) error {
args := m.Called()
return args.Error(0)
}
diff --git a/pkg/git/mock.go b/pkg/git/mock.go
index 6d4e28a..da24845 100644
--- a/pkg/git/mock.go
+++ b/pkg/git/mock.go
@@ -2,6 +2,7 @@ package git
import (
"context"
+
"github.com/akamai/cli/pkg/terminal"
"github.com/stretchr/testify/mock"
"gopkg.in/src-d/go-git.v4"
diff --git a/pkg/git/repository.go b/pkg/git/repository.go
index f38631b..9478b2b 100644
--- a/pkg/git/repository.go
+++ b/pkg/git/repository.go
@@ -3,12 +3,11 @@ package git
import (
"context"
"fmt"
- "gopkg.in/src-d/go-git.v4/plumbing"
- "gopkg.in/src-d/go-git.v4/plumbing/object"
-
- "gopkg.in/src-d/go-git.v4"
"github.com/akamai/cli/pkg/terminal"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
)
const (
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
index 0ae98de..dc8ea7c 100644
--- a/pkg/log/log_test.go
+++ b/pkg/log/log_test.go
@@ -3,13 +3,14 @@ package log
import (
"bytes"
"context"
- "github.com/apex/log"
- "github.com/stretchr/testify/require"
- "github.com/tj/assert"
"io/ioutil"
"os"
"regexp"
"testing"
+
+ "github.com/apex/log"
+ "github.com/stretchr/testify/require"
+ "github.com/tj/assert"
)
func TestSetupContext(t *testing.T) {
@@ -82,7 +83,7 @@ func TestWithCommand(t *testing.T) {
},
"output to file": {
logFile: "./testlogs.txt",
- expected: regexp.MustCompile(`\[[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\+[0-9]{2}:[0-9]{2}] ERROR abc[ ]*command=test`),
+ expected: regexp.MustCompile(`\[[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\+[0-9]{2}:[0-9]{2})?[Z]*] ERROR abc[ ]*command=test`),
},
}
for name, test := range tests {
diff --git a/pkg/packages/command_executor.go b/pkg/packages/command_executor.go
index df6a533..e729472 100644
--- a/pkg/packages/command_executor.go
+++ b/pkg/packages/command_executor.go
@@ -3,13 +3,22 @@ package packages
import (
"os"
"os/exec"
+ "runtime"
)
type (
executor interface {
+ // ExecCommand runs the given *exec.Cmd with combined output, if required
ExecCommand(cmd *exec.Cmd, withCombinedOutput ...bool) ([]byte, error)
+ // LookPath searches for an executable named file in the directories named by the PATH environment variable.
+ // If file contains a slash, it is tried directly and the PATH is not consulted.
+ // The result may be an absolute path or a path relative to the current directory.
+ // Just a wrapper around exec.LookPath(string)
LookPath(string) (string, error)
+ // FileExists checks if the given path exists. If there is an error, it will be of type *os.PathError.
FileExists(string) (bool, error)
+ // GetOS is a wrapper around runtime.GOOS
+ GetOS() string
}
defaultExecutor struct{}
@@ -22,6 +31,10 @@ func (d *defaultExecutor) ExecCommand(cmd *exec.Cmd, withCombinedOutput ...bool)
return cmd.Output()
}
+func (d *defaultExecutor) GetOS() string {
+ return runtime.GOOS
+}
+
func (d *defaultExecutor) LookPath(file string) (string, error) {
return exec.LookPath(file)
}
diff --git a/pkg/packages/command_executor_test.go b/pkg/packages/command_executor_test.go
index c7d0c89..afb8887 100644
--- a/pkg/packages/command_executor_test.go
+++ b/pkg/packages/command_executor_test.go
@@ -1,10 +1,11 @@
package packages
import (
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestExecCommand(t *testing.T) {
diff --git a/pkg/packages/golang.go b/pkg/packages/golang.go
index f7bd6a2..50fb9ab 100644
--- a/pkg/packages/golang.go
+++ b/pkg/packages/golang.go
@@ -25,12 +25,11 @@ import (
"regexp"
"strings"
- "github.com/fatih/color"
- "github.com/urfave/cli/v2"
-
"github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/tools"
"github.com/akamai/cli/pkg/version"
+ "github.com/fatih/color"
+ "github.com/urfave/cli/v2"
)
func (l *langManager) installGolang(ctx context.Context, dir, ver string, commands []string) error {
@@ -53,7 +52,7 @@ func (l *langManager) installGolang(ctx context.Context, dir, ver string, comman
return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "go", ver)
}
- if version.Compare(ver, matches[1]) == -1 {
+ if version.Compare(ver, matches[1]) == version.Greater {
logger.Debugf("Go Version found: %s", matches[1])
return fmt.Errorf("%w: required: %s:%s, have: %s. Please upgrade your runtime", ErrRuntimeMinimumVersionRequired, "go", ver, matches[1])
}
diff --git a/pkg/packages/golang_test.go b/pkg/packages/golang_test.go
index 6418fc7..d97f04b 100644
--- a/pkg/packages/golang_test.go
+++ b/pkg/packages/golang_test.go
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestInstallGolang(t *testing.T) {
diff --git a/pkg/packages/javascript.go b/pkg/packages/javascript.go
index cdf5c32..75cb010 100644
--- a/pkg/packages/javascript.go
+++ b/pkg/packages/javascript.go
@@ -51,7 +51,7 @@ func (l *langManager) installJavaScript(ctx context.Context, dir, ver string) er
return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "Node.js", ver)
}
- if version.Compare(ver, matches[1]) == -1 {
+ if version.Compare(ver, matches[1]) == version.Greater {
logger.Debugf("Node.js Version found: %s", matches[1])
return fmt.Errorf("%w: required: %s:%s, have: %s. Please upgrade your runtime", ErrRuntimeMinimumVersionRequired, "Node.js", ver, matches[1])
}
@@ -61,11 +61,7 @@ func (l *langManager) installJavaScript(ctx context.Context, dir, ver string) er
return err
}
- if err := installNodeDepsNpm(ctx, l.commandExecutor, dir); err != nil {
- return err
- }
-
- return nil
+ return installNodeDepsNpm(ctx, l.commandExecutor, dir)
}
func installNodeDepsYarn(ctx context.Context, cmdExecutor executor, dir string) error {
diff --git a/pkg/packages/javascript_test.go b/pkg/packages/javascript_test.go
index 976f1e9..8bade7c 100644
--- a/pkg/packages/javascript_test.go
+++ b/pkg/packages/javascript_test.go
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestInstallJavaScript(t *testing.T) {
diff --git a/pkg/packages/mock.go b/pkg/packages/mock.go
index 5d2d82b..e303f81 100644
--- a/pkg/packages/mock.go
+++ b/pkg/packages/mock.go
@@ -2,6 +2,7 @@ package packages
import (
"context"
+
"github.com/stretchr/testify/mock"
)
@@ -24,3 +25,32 @@ func (m *Mock) FindExec(_ context.Context, requirements LanguageRequirements, cm
}
return args.Get(0).([]string), args.Error(1)
}
+
+// FinishExecution mocks behavior of (*langManager) FinishExecution()
+func (m *Mock) FinishExecution(_ context.Context, languageRequirements LanguageRequirements, dirName string) {
+ m.Called(languageRequirements, dirName)
+}
+
+// PrepareExecution mocks behavior of (*langManager) PrepareExecution()
+func (m *Mock) PrepareExecution(_ context.Context, languageRequirements LanguageRequirements, dirName string) error {
+ args := m.Called(languageRequirements, dirName)
+ return args.Error(0)
+}
+
+// GetShell mocks behavior of (*langManager) GetShell()
+func (m *Mock) GetShell(goos string) (string, error) {
+ args := m.Called(goos)
+ return args.Get(0).(string), args.Error(0)
+}
+
+// GetOS mocks behavior of (*langManager) GetOS()
+func (m *Mock) GetOS() string {
+ m.Called()
+ return "GetOS()"
+}
+
+// FileExists mocks behavior of (*langManager) FileExists()
+func (m *Mock) FileExists(path string) (bool, error) {
+ args := m.Called(path)
+ return args.Bool(0), args.Error(1)
+}
diff --git a/pkg/packages/package.go b/pkg/packages/package.go
index 06f1100..580b873 100644
--- a/pkg/packages/package.go
+++ b/pkg/packages/package.go
@@ -3,14 +3,33 @@ package packages
import (
"context"
"errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+
"github.com/akamai/cli/pkg/log"
+ "github.com/akamai/cli/pkg/tools"
+ "github.com/akamai/cli/pkg/version"
)
type (
// LangManager allows operations on different programming languages
LangManager interface {
+ // Install builds and installs contents of a directory based on provided language requirements
Install(ctx context.Context, dir string, requirements LanguageRequirements, commands []string) error
+ // FindExec locates language's CLI executable
FindExec(ctx context.Context, requirements LanguageRequirements, cmdExec string) ([]string, error)
+ // PrepareExecution performs any operation required before the external command invocation
+ PrepareExecution(ctx context.Context, languageRequirements LanguageRequirements, dirName string) error
+ // FinishExecution perform any required tear down steps for external command invocation
+ FinishExecution(ctx context.Context, languageRequirements LanguageRequirements, dirName string)
+ // GetShell returns the available shell
+ GetShell(goos string) (string, error)
+ // GetOS is a wrapper around executor.GetOS()
+ GetOS() string
+ // FileExists checks if the given path exists
+ FileExists(path string) (bool, error)
}
// LanguageRequirements contains version requirements for all supported programming languages
@@ -43,6 +62,19 @@ var (
ErrPackageManagerExec = errors.New("unable to execute package manager")
ErrPackageNeedsReinstall = errors.New("you must reinstall this package to continue")
ErrPackageCompileFailure = errors.New("unable to build binary")
+ ErrPackageExecutableNotFound = errors.New("package executable not found")
+ ErrVirtualEnvCreation = errors.New("unable to create virtual environment")
+ ErrVirtualEnvActivation = errors.New("unable to activate virtual environment")
+ ErrVenvNotFound = errors.New("venv python package not found. Please verify your setup")
+ ErrPipNotFound = errors.New("pip not found. Please verify your setup")
+ ErrRequirementsTxtNotFound = errors.New("requirements.txt not found in the subcommand")
+ ErrRequirementsInstall = errors.New("failed to install required python packages from requirements.txt")
+ ErrPipUpgrade = errors.New("unable to install/upgrade pip")
+ ErrPipSetuptoolsUpgrade = errors.New("unable to execute 'python3 -m pip install --user --no-cache --upgrade pip setuptools'")
+ ErrNoExeFound = errors.New("no executables found")
+ ErrOSNotSupported = errors.New("OS not supported")
+ ErrPythonVersionNotSupported = errors.New("python version not supported")
+ ErrDirectoryCreation = errors.New("unable to create directory")
)
type langManager struct {
@@ -56,25 +88,50 @@ func NewLangManager() LangManager {
}
}
-// Install builds and installs contents of a directory based on provided language requirements
-func (l *langManager) Install(ctx context.Context, dir string, reqs LanguageRequirements, commands []string) error {
+func (l *langManager) GetShell(goos string) (string, error) {
+ var pathErr *os.PathError
+
+ switch goos {
+ case "windows":
+ return "", nil
+ case "linux", "darwin":
+ sh, err := lookForBins(l.commandExecutor, "bash", "sh")
+ if err != nil && errors.As(err, &pathErr) && pathErr.Err != syscall.ENOENT {
+ return "", err
+ }
+ if err == nil {
+ return sh, nil
+ }
+ }
+
+ return "", ErrOSNotSupported
+}
+
+func (l *langManager) Install(ctx context.Context, pkgSrcPath string, reqs LanguageRequirements, commands []string) error {
lang, requirements := determineLangAndRequirements(reqs)
switch lang {
case PHP:
- return l.installPHP(ctx, dir, requirements)
+ return l.installPHP(ctx, pkgSrcPath, requirements)
case Javascript:
- return l.installJavaScript(ctx, dir, requirements)
+ return l.installJavaScript(ctx, pkgSrcPath, requirements)
case Ruby:
- return l.installRuby(ctx, dir, requirements)
+ return l.installRuby(ctx, pkgSrcPath, requirements)
case Python:
- return l.installPython(ctx, dir, requirements)
+ pkgVenvPath, err := tools.GetPkgVenvPath(filepath.Base(pkgSrcPath))
+ if err != nil {
+ return err
+ }
+ err = l.installPython(ctx, pkgVenvPath, pkgSrcPath, requirements)
+ if err != nil {
+ _ = os.RemoveAll(pkgVenvPath)
+ }
+ return err
case Go:
- return l.installGolang(ctx, dir, requirements, commands)
+ return l.installGolang(ctx, pkgSrcPath, requirements, commands)
}
return ErrUnknownLang
}
-// FindExec locates language's CLI executable
func (l *langManager) FindExec(ctx context.Context, reqs LanguageRequirements, cmdExec string) ([]string, error) {
logger := log.FromContext(ctx)
lang, requirements := determineLangAndRequirements(reqs)
@@ -86,14 +143,18 @@ func (l *langManager) FindExec(ctx context.Context, reqs LanguageRequirements, c
bin, err := l.commandExecutor.LookPath("node")
if err != nil {
bin, err = l.commandExecutor.LookPath("nodejs")
+ if err != nil {
+ return nil, err
+ }
}
return []string{bin, cmdExec}, nil
case Python:
- bin, err := findPythonBin(ctx, l.commandExecutor, requirements)
+ cmdName := filepath.Base(cmdExec)
+ pythonBin, err := findPythonBin(ctx, l.commandExecutor, requirements, cmdName)
if err != nil {
return nil, err
}
- return []string{bin, cmdExec}, nil
+ return []string{pythonBin, cmdExec}, nil
case Undefined:
logger.Debugf("command language is not defined")
return []string{cmdExec}, nil
@@ -102,6 +163,47 @@ func (l *langManager) FindExec(ctx context.Context, reqs LanguageRequirements, c
}
}
+func (l *langManager) PrepareExecution(ctx context.Context, languageRequirements LanguageRequirements, dirName string) error {
+ if languageRequirements.Python != "" {
+ logger := log.FromContext(ctx)
+
+ logger.Debugf("Validating python dependencies")
+ python34Bin, pipBin, err := l.validatePythonDeps(ctx, logger, languageRequirements.Python, dirName)
+ if err != nil {
+ return err
+ }
+
+ srcPath, err := tools.GetAkamaiCliSrcPath()
+ if err != nil {
+ return err
+ }
+
+ pkgSrcPath := filepath.Join(srcPath, dirName)
+
+ pkgVePath, err := tools.GetPkgVenvPath(dirName)
+ if err != nil {
+ return err
+ }
+
+ logger.Debugf("Adding any missing element to the virtual environment")
+ return l.setup(ctx, pkgVePath, pkgSrcPath, python34Bin, pipBin, languageRequirements.Python, true)
+ }
+ return nil
+}
+
+func (l *langManager) FinishExecution(ctx context.Context, reqs LanguageRequirements, dirName string) {
+ if reqs.Python != "" {
+ if version.Compare(reqs.Python, "3.0.0") != version.Smaller {
+ venvPath, _ := tools.GetPkgVenvPath(dirName)
+ l.deactivateVirtualEnvironment(ctx, venvPath, reqs.Python)
+ }
+ }
+}
+
+func (l *langManager) GetOS() string {
+ return l.commandExecutor.GetOS()
+}
+
func determineLangAndRequirements(reqs LanguageRequirements) (string, string) {
if reqs.Php != "" {
return PHP, reqs.Php
@@ -120,8 +222,21 @@ func determineLangAndRequirements(reqs LanguageRequirements) (string, string) {
}
if reqs.Python != "" {
- return Python, reqs.Python
+ return Python, translateWildcards(reqs.Python)
}
return Undefined, ""
}
+
+func translateWildcards(version string) string {
+ version = strings.ReplaceAll(version, "*", "0")
+ splitVersion := strings.Split(version, ".")
+ for len(splitVersion) < 3 {
+ splitVersion = append(splitVersion, "0")
+ }
+ return strings.Join(splitVersion, ".")
+}
+
+func (l *langManager) FileExists(path string) (bool, error) {
+ return l.commandExecutor.FileExists(path)
+}
diff --git a/pkg/packages/package_test.go b/pkg/packages/package_test.go
index da0a77c..d77e83a 100644
--- a/pkg/packages/package_test.go
+++ b/pkg/packages/package_test.go
@@ -3,12 +3,12 @@ package packages
import (
"context"
"fmt"
+ "os/exec"
+ "testing"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "os/exec"
-
- "testing"
)
type mocked struct {
@@ -52,38 +52,13 @@ func TestLangManager_FindExec(t *testing.T) {
},
expected: []string{"/test/nodejs", "test"},
},
- "python command, default version, python command found": {
- givenReqs: LanguageRequirements{
- Python: "*",
- },
- givenCmdExec: "test",
- init: func(m *mocked) {
- m.On("LookPath", "python3").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python2").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python").Return("/test/python", nil)
- },
- expected: []string{"/test/python", "test"},
- },
- "python command, default version, not found": {
- givenReqs: LanguageRequirements{
- Python: "*",
- },
- givenCmdExec: "test",
- init: func(m *mocked) {
- m.On("LookPath", "python3").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python2").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python").Return("", fmt.Errorf("not found"))
- },
- withError: true,
- },
"python command, version 3, binary found": {
givenReqs: LanguageRequirements{
Python: "3.0.0",
},
givenCmdExec: "test",
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python").Return("/test/python", nil)
+ m.On("LookPath", "python3").Return("/test/python", nil)
},
expected: []string{"/test/python", "test"},
},
@@ -94,7 +69,8 @@ func TestLangManager_FindExec(t *testing.T) {
givenCmdExec: "test",
init: func(m *mocked) {
m.On("LookPath", "python3").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python").Return("", fmt.Errorf("not found"))
+ m.On("LookPath", "python3.exe").Return("", fmt.Errorf("not found"))
+ m.On("LookPath", "py.exe").Return("", fmt.Errorf("not found"))
},
withError: true,
},
@@ -108,15 +84,27 @@ func TestLangManager_FindExec(t *testing.T) {
},
expected: []string{"/test/python2", "test"},
},
+ "python command, version 2, python3 not found": {
+ givenReqs: LanguageRequirements{
+ Python: "2.0.0",
+ },
+ givenCmdExec: "test",
+ init: func(m *mocked) {
+ m.On("LookPath", "python2").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "python2.exe").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "py.exe").Return("", fmt.Errorf("not found")).Once()
+ },
+ withError: true,
+ },
"python command, version 2, not found": {
givenReqs: LanguageRequirements{
Python: "2.0.0",
},
givenCmdExec: "test",
init: func(m *mocked) {
- m.On("LookPath", "python2").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python").Return("", fmt.Errorf("not found"))
- m.On("LookPath", "python3").Return("", fmt.Errorf("not found"))
+ m.On("LookPath", "python2").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "python2.exe").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "py.exe").Return("", fmt.Errorf("not found")).Once()
},
withError: true,
},
@@ -175,3 +163,49 @@ func (m *mocked) FileExists(path string) (bool, error) {
args := m.Called(path)
return args.Bool(0), args.Error(1)
}
+
+func (m *mocked) GetOS() string {
+ args := m.Called()
+ return args.String(0)
+}
+
+func TestDetermineLangAndRequirements(t *testing.T) {
+ tests := map[string]struct {
+ reqs LanguageRequirements
+ language string
+ version string
+ }{
+ "undefined": {
+ language: Undefined,
+ version: "",
+ },
+ "concrete Python version": {
+ reqs: LanguageRequirements{Python: "2.7.10"},
+ language: Python,
+ version: "2.7.10",
+ },
+ "Python with wildcard": {
+ reqs: LanguageRequirements{Python: "3.0.*"},
+ language: Python,
+ version: "3.0.0",
+ },
+ "Python with short wildcard": {
+ reqs: LanguageRequirements{Python: "3.*"},
+ language: Python,
+ version: "3.0.0",
+ },
+ "Python pure wildcard": {
+ reqs: LanguageRequirements{Python: "*"},
+ language: Python,
+ version: "0.0.0",
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ language, version := determineLangAndRequirements(test.reqs)
+ assert.Equal(t, test.language, language)
+ assert.Equal(t, test.version, version)
+ })
+ }
+}
diff --git a/pkg/packages/php.go b/pkg/packages/php.go
index 91bc18c..92f7d9d 100644
--- a/pkg/packages/php.go
+++ b/pkg/packages/php.go
@@ -47,17 +47,13 @@ func (l *langManager) installPHP(ctx context.Context, dir, cmdReq string) error
return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "php", cmdReq)
}
- if version.Compare(cmdReq, matches[1]) == -1 {
+ if version.Compare(cmdReq, matches[1]) == version.Greater {
logger.Debugf("PHP Version found: %s", matches[1])
return fmt.Errorf("%w: required: %s:%s, have: %s. Please upgrade your runtime", ErrRuntimeMinimumVersionRequired, "php", cmdReq, matches[1])
}
}
- if err := installPHPDepsComposer(ctx, l.commandExecutor, bin, dir); err != nil {
- return err
- }
-
- return nil
+ return installPHPDepsComposer(ctx, l.commandExecutor, bin, dir)
}
func installPHPDepsComposer(ctx context.Context, cmdExecutor executor, phpBin, dir string) error {
diff --git a/pkg/packages/php_test.go b/pkg/packages/php_test.go
index c620efb..9482e95 100644
--- a/pkg/packages/php_test.go
+++ b/pkg/packages/php_test.go
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestInstallPHP(t *testing.T) {
diff --git a/pkg/packages/python.go b/pkg/packages/python.go
index 204e925..d8d426c 100644
--- a/pkg/packages/python.go
+++ b/pkg/packages/python.go
@@ -26,46 +26,377 @@ import (
"strings"
"github.com/akamai/cli/pkg/log"
+ "github.com/akamai/cli/pkg/terminal"
+ "github.com/akamai/cli/pkg/tools"
"github.com/akamai/cli/pkg/version"
)
-func (l *langManager) installPython(ctx context.Context, dir, cmdReq string) error {
+var (
+ pythonVersionPattern = `Python (\d+\.\d+\.\d+).*`
+ pythonVersionRegex = regexp.MustCompile(pythonVersionPattern)
+ pipVersionPattern = `^pip \d{1,2}\..+ \(python \d\.\d\)`
+ venvHelpPattern = `usage: venv `
+ pipVersionRegex = regexp.MustCompile(pipVersionPattern)
+ venvHelpRegex = regexp.MustCompile(venvHelpPattern)
+)
+
+func (l *langManager) installPython(ctx context.Context, venvPath, srcPath, requiredPy string) error {
logger := log.FromContext(ctx)
- pythonBin, err := findPythonBin(ctx, l.commandExecutor, cmdReq)
+ pythonBin, pipBin, err := l.validatePythonDeps(ctx, logger, requiredPy, filepath.Base(srcPath))
if err != nil {
+ logger.Errorf("%v", err)
+ return err
+ }
+
+ if err = l.setup(ctx, venvPath, srcPath, pythonBin, pipBin, requiredPy, false); err != nil {
+ logger.Errorf("%v", err)
return err
}
- pipBin, err := findPipBin(ctx, l.commandExecutor, cmdReq)
+
+ return nil
+}
+
+// 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 {
+ 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)
+ }
+ logger.Debugf("All virtualenv dependencies successfully installed")
+ }()
+
+ veExists, err := l.commandExecutor.FileExists(pkgVenvPath)
+ if err != nil {
+ return err
+ }
+
+ if !passthru || !veExists {
+ logger.Debugf("the virtual environment %s does not exist yet - installing dependencies", pkgVenvPath)
+
+ // upgrade pip and setuptools
+ if err := l.upgradePipAndSetuptools(ctx, python3Bin); err != nil {
+ return err
+ }
+
+ // create virtual environment
+ if err := l.createVirtualEnvironment(ctx, python3Bin, pkgVenvPath); err != nil {
+ return err
+ }
+ }
+
+ // activate virtual environment
+ if err := l.activateVirtualEnvironment(ctx, pkgVenvPath); err != nil {
+ return err
+ }
+
+ // install packages from requirements.txt
+ vePy, err := l.getVePython(pkgVenvPath)
+ if err != nil {
+ return err
+ }
+ if err := l.installVeRequirements(ctx, srcPath, pkgVenvPath, vePy); err != nil {
+ return err
+ }
+ case version.Smaller:
+ // no virtualenv for python 2.x
+ return installPythonDepsPip(ctx, l.commandExecutor, pipBin, srcPath)
+ }
+
+ return nil
+}
+
+func (l *langManager) getVePython(vePath string) (string, error) {
+ switch l.GetOS() {
+ case "windows":
+ return filepath.Join(vePath, "System", "python.exe"), nil
+ case "linux", "darwin":
+ return filepath.Join(vePath, "bin", "python"), nil
+ default:
+ return "", ErrOSNotSupported
+ }
+}
+
+func (l *langManager) installVeRequirements(ctx context.Context, srcPath, vePath, py3Bin string) error {
+ logger := log.FromContext(ctx)
+
+ requirementsPath := filepath.Join(srcPath, "requirements.txt")
+ if ok, _ := l.commandExecutor.FileExists(requirementsPath); !ok {
+ return ErrRequirementsTxtNotFound
+ }
+ logger.Info("requirements.txt found, running pip package manager")
+
+ shell, err := l.GetShell(l.GetOS())
if err != nil {
return err
}
+ if shell == "" {
+ // windows
+ pipPath := filepath.Join(vePath, "Scripts", "pip.exe")
+ if output, err := l.commandExecutor.ExecCommand(&exec.Cmd{
+ Path: pipPath,
+ Args: []string{pipPath, "install", "--upgrade", "--ignore-installed", "-r", requirementsPath},
+ }, true); err != nil {
+ term := terminal.Get(ctx)
+ _, _ = term.Writeln(string(output))
+ logger.Errorf("failed to run pip install --upgrade --ignore-installed -r requirements.txt")
+ return fmt.Errorf("%v: %s", ErrRequirementsInstall, string(output))
+ }
+ return nil
+ }
- if cmdReq != "" && cmdReq != "*" {
- cmd := exec.Command(pythonBin, "--version")
- output, _ := l.commandExecutor.ExecCommand(cmd, true)
- logger.Debugf("%s --version: %s", pythonBin, bytes.ReplaceAll(output, []byte("\n"), []byte("")))
- r := regexp.MustCompile(`Python (\d+\.\d+\.\d+).*`)
- matches := r.FindStringSubmatch(string(output))
+ if output, err := l.commandExecutor.ExecCommand(&exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "pip", "install", "--upgrade", "--ignore-installed", "-r", requirementsPath},
+ }, true); err != nil {
+ logger.Errorf("failed to run pip install --upgrade --ignore-installed -r requirements.txt")
+ logger.Errorf(string(output))
+ return fmt.Errorf("%v: %v", ErrRequirementsInstall, string(output))
+ }
+ return nil
+}
+
+/*
+validatePythonDeps does system dependencies validation based on the required python version
+
+It returns:
+
+* route to required python executable
+
+* route to pip executable, for modules which require python < v3
+
+* error, if any
+*/
+func (l *langManager) validatePythonDeps(ctx context.Context, logger log.Logger, requiredPy, name string) (string, string, error) {
+ switch version.Compare(requiredPy, "3.0.0") {
+ case version.Smaller:
+ // v2 required -> no virtualenv
+ logger.Debugf("Validating dependencies for python 2.x module")
+ pythonBin, err := findPythonBin(ctx, l.commandExecutor, requiredPy, "")
+ if err != nil {
+ logger.Errorf("Python >= 2 (and < 3.0) not found in the system. Please verify your setup", requiredPy)
+ return "", "", err
+ }
+
+ if err := l.resolveBinVersion(pythonBin, requiredPy, "--version", logger); err != nil {
+ return "", "", err
+ }
+
+ pipBin, err := findPipBin(ctx, l.commandExecutor, requiredPy)
+ if err != nil {
+ return pythonBin, "", err
+ }
+ return pythonBin, pipBin, nil
+ case version.Greater, version.Equals:
+ // v3 required -> virtualenv
+ // requirements for setting up VE: python3, pip3, venv
+ logger.Debugf("Validating dependencies for python %s module", requiredPy)
+ pythonBin, err := findPythonBin(ctx, l.commandExecutor, requiredPy, name)
+ if err != nil {
+ logger.Errorf("Python >= %s not found in the system. Please verify your setup", requiredPy)
+ return "", "", err
+ }
+
+ if err := l.resolveBinVersion(pythonBin, requiredPy, "--version", logger); err != nil {
+ return "", "", err
+ }
+
+ // validate that the use has python pip package installed
+ if err = l.findPipPackage(ctx, requiredPy, pythonBin); err != nil {
+ logger.Errorf("Pip not found in the system. Please verify your setup")
+ return "", "", err
+ }
+
+ // validate that venv module is present
+ if err = l.findVenvPackage(ctx, pythonBin); err != nil {
+ logger.Errorf("Python venv module not found in the system. Please verify your setup")
+ return "", "", err
+ }
+
+ return pythonBin, "", nil
+ default:
+ // not supported
+ logger.Errorf("%s: %s", ErrPythonVersionNotSupported.Error(), requiredPy)
+ return "", "", fmt.Errorf("%v: %s", ErrPythonVersionNotSupported, requiredPy)
+ }
+}
+
+func (l *langManager) findVenvPackage(ctx context.Context, pythonBin string) error {
+ logger := log.FromContext(ctx)
+ cmd := exec.Command(pythonBin, "-m", "venv", "--version")
+ output, _ := l.commandExecutor.ExecCommand(cmd, true)
+ logger.Debugf("%s %s %s: %s", pythonBin, "-m venv --version", bytes.ReplaceAll(output, []byte("\n"), []byte("")))
+ matches := venvHelpRegex.FindStringSubmatch(string(output))
+ if len(matches) == 0 {
+ return fmt.Errorf("%v: %s", ErrVenvNotFound, bytes.ReplaceAll(output, []byte("\n"), []byte("")))
+ }
+ return nil
+}
+
+func (l *langManager) findPipPackage(ctx context.Context, requiredPy string, pythonBin string) error {
+ compare := version.Compare(requiredPy, "3.0.0")
+ if compare == version.Greater || compare == version.Equals {
+ logger := log.FromContext(ctx)
+
+ // find pip python package, not pip executable
+ cmd := exec.Command(pythonBin, "-m", "pip", "--version")
+ output, _ := l.commandExecutor.ExecCommand(cmd, true)
+ logger.Debugf("%s %s %s: %s", pythonBin, "-m pip --version", bytes.ReplaceAll(output, []byte("\n"), []byte("")))
+ matches := pipVersionRegex.FindStringSubmatch(string(output))
if len(matches) == 0 {
- return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "python", cmdReq)
+ return fmt.Errorf("%v: %s", ErrPipNotFound, bytes.ReplaceAll(output, []byte("\n"), []byte("")))
}
+ }
- if version.Compare(cmdReq, matches[1]) == -1 {
- logger.Debugf("Python Version found: %s", matches[1])
- return fmt.Errorf("%w: required: %s:%s, have: %s. Please upgrade your runtime", ErrRuntimeMinimumVersionRequired, "python", cmdReq, matches[1])
+ return nil
+}
+
+func (l *langManager) activateVirtualEnvironment(ctx context.Context, pkgVenvPath string) error {
+ logger := log.FromContext(ctx)
+ logger.Debugf("Activating Python virtualenv: %s", pkgVenvPath)
+ oS := l.GetOS()
+ interpreter, err := l.GetShell(oS)
+ if err != nil {
+ logger.Errorf("cannot determine OS shell")
+ return err
+ }
+ cmd := &exec.Cmd{}
+ if oS == "windows" {
+ activate := filepath.Join(pkgVenvPath, "Scripts", "activate.bat")
+ cmd.Path = activate
+ cmd.Args = []string{}
+ } else {
+ cmd.Path = interpreter
+ cmd.Args = []string{"source", filepath.Join(pkgVenvPath, "bin", "activate")}
+ }
+ if output, err := l.commandExecutor.ExecCommand(cmd, true); err != nil {
+ logger.Errorf("%w: %v", ErrVirtualEnvActivation, string(output))
+ return fmt.Errorf("%w: %s", ErrVirtualEnvActivation, string(output))
+ }
+ logger.Debugf("Python virtualenv %s active", pkgVenvPath)
+
+ return nil
+}
+
+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)
+ logger.Debugf("Deactivating virtual environment %s", dir)
+ cmd := &exec.Cmd{}
+ oS := l.GetOS()
+ if oS == "windows" {
+ logger.Debugf("windows detected, executing deactivate.bat")
+ deactivate := filepath.Join(dir, "Scripts", "deactivate.bat")
+ cmd.Path = deactivate
+ cmd.Args = []string{deactivate}
+ } else {
+ cmd.Path, _ = l.GetShell(oS)
+ cmd.Args = []string{"deactivate"}
+ }
+ // errors are ignored at this step, as we may be trying to deactivate a non existent or not active virtual environment
+ if _, err := l.commandExecutor.ExecCommand(cmd, true); err == nil {
+ logger.Debugf("Python virtualenv deactivated")
+ } else {
+ logger.Errorf("Error deactivating VE %s: %v", dir, err)
}
}
+}
- if err := installPythonDepsPip(ctx, l.commandExecutor, pipBin, dir); err != nil {
+func (l *langManager) resolveBinVersion(bin, cmdReq, arg string, logger log.Logger) error {
+ cmd := exec.Command(bin, arg)
+ output, err := l.commandExecutor.ExecCommand(cmd, true)
+ if err != nil {
return err
}
+ logger.Debugf("%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)
+ }
+ switch version.Compare(cmdReq, matches[1]) {
+ case version.Greater:
+ // python required > python installed
+ logger.Errorf("%s version found: %s", bin, matches[1])
+ return fmt.Errorf("%v: required: %s:%s, have: %s. Please install the required Python branch", ErrRuntimeMinimumVersionRequired, bin, cmdReq, matches[1])
+ case version.Smaller:
+ // python required < python installed
+ if version.Compare(cmdReq, "3.0.0") == 1 && version.Compare(matches[1], "3.0.0") <= 0 {
+ // required: py2; found: py3. The user still needs to install py2
+ logger.Errorf("Python version %s found, %s version required. Please, install the %s Python branch.")
+ return fmt.Errorf("%v: Please install the following Python branch: %s", ErrRuntimeNotFound, cmdReq)
+ }
+ return nil
+ case version.Equals:
+ return nil
+ }
+ return ErrPythonVersionNotSupported
+}
+
+func (l *langManager) upgradePipAndSetuptools(ctx context.Context, python3Bin string) error {
+ logger := log.FromContext(ctx)
+
+ // if python3 > v3.4, ensure pip
+ if err := l.resolveBinVersion(python3Bin, "3.4.0", "--version", logger); err != nil && errors.As(err, &ErrRuntimeNotFound) {
+ return err
+ }
+
+ logger.Debugf("Installing/upgrading pip")
+
+ // ensure pip is present
+ cmdPip := exec.Command(python3Bin, "-m", "ensurepip", "--upgrade")
+ if output, err := l.commandExecutor.ExecCommand(cmdPip, true); err != nil {
+ logger.Warnf("%w: %s", ErrPipUpgrade, string(output))
+ }
+
+ // upgrade pip & setuptools
+ logger.Debugf("Installing/upgrading pip & setuptools")
+ cmdSetuptools := exec.Command(python3Bin, "-m", "pip", "install" /*, "--user"*/, "--no-cache", "--upgrade", "pip", "setuptools")
+ if output, err := l.commandExecutor.ExecCommand(cmdSetuptools, true); err != nil {
+ logger.Errorf("%v: %s", ErrPipSetuptoolsUpgrade, string(output))
+ return fmt.Errorf("%v: %s", ErrPipSetuptoolsUpgrade, string(output))
+ }
+
+ return nil
+}
+
+func (l *langManager) createVirtualEnvironment(ctx context.Context, python3Bin string, pkgVenvPath string) error {
+ logger := log.FromContext(ctx)
+
+ // check if the .akamai-cli/venv directory exists - create it otherwise
+ venvPath := filepath.Dir(pkgVenvPath)
+ if exists, err := l.commandExecutor.FileExists(venvPath); err == nil && !exists {
+ logger.Debugf("%s does not exist; let's create it", venvPath)
+ if err := os.Mkdir(venvPath, 0755); err != nil {
+ logger.Errorf("%v %s: %v", ErrDirectoryCreation, venvPath, err)
+ return fmt.Errorf("%v %s: %v", ErrDirectoryCreation, venvPath, err)
+ }
+ logger.Debugf("%s directory created", venvPath)
+ } else {
+ if err != nil {
+ return err
+ }
+ }
+
+ logger.Debugf("Creating python virtualenv: %s", pkgVenvPath)
+ cmdVenv := exec.Command(python3Bin, "-m", "venv", pkgVenvPath)
+ if output, err := l.commandExecutor.ExecCommand(cmdVenv, true); err != nil {
+ logger.Errorf("%v %s: %s", ErrVirtualEnvCreation, pkgVenvPath, string(output))
+ return fmt.Errorf("%v %s: %s", ErrVirtualEnvCreation, pkgVenvPath, string(output))
+ }
+ logger.Debugf("Python virtualenv successfully created: %s", pkgVenvPath)
return nil
}
-func findPythonBin(ctx context.Context, cmdExecutor executor, ver string) (string, error) {
+func findPythonBin(ctx context.Context, cmdExecutor executor, ver, name string) (string, error) {
logger := log.FromContext(ctx)
var err error
@@ -76,28 +407,39 @@ func findPythonBin(ctx context.Context, cmdExecutor executor, ver string) (strin
logger.Debugf("Python binary found: %s", bin)
}
}()
- if ver == "" || ver == "*" {
- bin, err = lookForBins(cmdExecutor, "python3", "python2", "python")
+ if version.Compare("3.0.0", ver) != version.Greater {
+ // looking for python3 or py (windows)
+ bin, err = lookForBins(cmdExecutor, "python3", "python3.exe", "py.exe")
if err != nil {
- return "", fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "python")
+ return "", fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "python 3")
+ }
+ vePath, _ := tools.GetPkgVenvPath(name)
+ if _, err := os.Stat(vePath); !os.IsNotExist(err) {
+ if cmdExecutor.GetOS() == "windows" {
+ bin = filepath.Join(vePath, "Scripts", "python.exe")
+ } else {
+ bin = filepath.Join(vePath, "bin", "python")
+ }
}
return bin, nil
}
- if version.Compare("3.0.0", ver) != -1 {
- bin, err = lookForBins(cmdExecutor, "python3", "python")
+ if version.Compare("2.0.0", ver) != version.Greater {
+ // looking for python2 or py (windows) - no virtualenv
+ bin, err = lookForBins(cmdExecutor, "python2", "python2.exe", "py.exe")
if err != nil {
- return "", fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "python 3")
+ return "", fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "python 2")
}
return bin, nil
}
- bin, err = lookForBins(cmdExecutor, "python2", "python", "python3")
+ // looking for any version
+ bin, err = lookForBins(cmdExecutor, "python2", "python", "python3", "py.exe", "python.exe")
if err != nil {
return "", fmt.Errorf("%w: %s. Please verify if the executable is included in your PATH", ErrRuntimeNotFound, "python")
}
return bin, nil
}
-func findPipBin(ctx context.Context, cmdExecutor executor, ver string) (string, error) {
+func findPipBin(ctx context.Context, cmdExecutor executor, requiredPy string) (string, error) {
logger := log.FromContext(ctx)
var bin string
@@ -107,24 +449,19 @@ func findPipBin(ctx context.Context, cmdExecutor executor, ver string) (string,
logger.Debugf("Pip binary found: %s", bin)
}
}()
- if ver == "" || ver == "*" {
- bin, err = lookForBins(cmdExecutor, "pip3", "pip2", "pip")
+ switch version.Compare(requiredPy, "3.0.0") {
+ case version.Greater, version.Equals:
+ bin, err = lookForBins(cmdExecutor, "pip3", "pip3.exe")
if err != nil {
- return "", fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "pip")
+ return "", fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "pip3")
}
- return bin, nil
- }
- if version.Compare("3.0.0", ver) != -1 {
- bin, err = lookForBins(cmdExecutor, "pip3", "pip")
+ case version.Smaller:
+ bin, err = lookForBins(cmdExecutor, "pip2")
if err != nil {
- return "", fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "pip3")
+ return "", fmt.Errorf("%v, %s", ErrPackageManagerNotFound, "pip2")
}
- return bin, nil
- }
- bin, err = lookForBins(cmdExecutor, "pip2", "pip", "pip3")
- if err != nil {
- return "", fmt.Errorf("%w: %s", ErrPackageManagerNotFound, "pip")
}
+
return bin, nil
}
@@ -139,7 +476,7 @@ func installPythonDepsPip(ctx context.Context, cmdExecutor executor, bin, dir st
if err := os.Setenv("PYTHONUSERBASE", dir); err != nil {
return err
}
- args := []string{bin, "install", "--user", "--ignore-installed", "-r", "requirements.txt"}
+ args := []string{bin, "install", "--user", "--ignore-installed", "-r", filepath.Join(dir, "requirements.txt")}
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
if _, err := cmdExecutor.ExecCommand(cmd); err != nil {
diff --git a/pkg/packages/python_test.go b/pkg/packages/python_test.go
index 3aa2fd2..98cfe0d 100644
--- a/pkg/packages/python_test.go
+++ b/pkg/packages/python_test.go
@@ -4,139 +4,368 @@ import (
"context"
"errors"
"fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
+func TestPythonVersionRegexp(t *testing.T) {
+ tests := map[string]struct {
+ input string
+ isMatch bool
+ }{
+ "good input - python 3.4": {
+ input: "Python 3.4.0",
+ isMatch: true,
+ },
+ "good input - python 2.7": {
+ input: "Python 2.7.10",
+ isMatch: true,
+ },
+ "good input - trailing characters": {
+ input: "Python 3.4.0 ",
+ isMatch: true,
+ },
+ "good input - trailing line break": {
+ input: "Python 3.4.0\n",
+ isMatch: true,
+ },
+ "bad input - no version": {
+ input: "Python",
+ isMatch: false,
+ },
+ "bad input - no python": {
+ input: "3.4.0",
+ isMatch: false,
+ },
+ "bad input - no space": {
+ input: "Python3.4.0",
+ isMatch: false,
+ },
+ "bad input - rubbish": {
+ input: "random string",
+ isMatch: false,
+ },
+ "bad input - empty": {
+ input: "",
+ isMatch: false,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ if test.isMatch {
+ assert.Regexp(t, pythonVersionRegex, test.input)
+ } else {
+ assert.NotRegexp(t, pythonVersionRegex, test.input)
+ }
+ })
+ }
+}
+
+func TestPipVersionRegexp(t *testing.T) {
+ tests := map[string]struct {
+ input string
+ isMatch bool
+ }{
+ "good input - python 3.9": {
+ input: "pip 21.1.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)",
+ isMatch: true,
+ },
+ "good input - trailing line break": {
+ input: "pip 21.1.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)\n",
+ isMatch: true,
+ },
+ "bad input - no version": {
+ input: "pip from /usr/local/lib/python3.9/site-packages/pip (python 3.9)",
+ isMatch: false,
+ },
+ "bad input - no python": {
+ input: "pip 21.1.1 from /usr/local/lib/python3.9/site-packages/pip",
+ isMatch: false,
+ },
+ "bad input - no space": {
+ input: "pip21.1.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)",
+ isMatch: false,
+ },
+ "bad input - rubbish": {
+ input: "random string",
+ isMatch: false,
+ },
+ "bad input - empty": {
+ input: "",
+ isMatch: false,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ if test.isMatch {
+ assert.Regexp(t, pipVersionRegex, test.input)
+ } else {
+ assert.NotRegexp(t, pipVersionRegex, test.input)
+ }
+ })
+ }
+}
+
func TestInstallPython(t *testing.T) {
+ pip2Bin := "/test/pip2"
+ bashBin := "/test/bash"
+ py2Bin := "/test/python2"
+ py3Bin := "/test/python3"
+ py3VeBin := "veDir/bin/python"
+ py3PipVersion := "pip 21.0.1 from /usr/lib/python3.8/site-packages/pip (python 3.8)"
+ py3VenvHelp := `
+usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
+ [--upgrade] [--without-pip] [--prompt PROMPT]
+ ENV_DIR [ENV_DIR ...]
+venv: error: the following arguments are required: ENV_DIR
+`
+ py3BinWindows := "c:/Program files/Python/python3.exe"
+ py3WindowsPipVersion := "pip 20.1.3 from c:\\Program Files\\WindowsApps\\" +
+ "PythonSoftwareFoundation.Python.3.9_3.9.1264.0_x64__qbz5n2kfra8p0\\" +
+ "lib\\site-packages\\pip (python 3.4)"
+ py2Version := "Python 2.7.16"
+ py34Version := "Python 3.4.0"
+ ver2 := "2.0.0"
+ ver3 := "3.0.0"
+ ver355 := "3.5.5"
+ srcDir := "testDir"
+ veDir := "veDir"
+ requirementsFile := "testDir/requirements.txt"
+ winVePipPath := "veDir/Scripts/pip.exe"
+ winDeactivatePath := "veDir/Scripts/deactivate.bat"
+
tests := map[string]struct {
- givenDir string
- givenVer string
- init func(*mocked)
- withError error
+ givenDir string
+ veDir string
+ requiredPy string
+ goos string
+ init func(*mocked)
+ withError *error
}{
- "with version 3 and pip": {
- givenDir: "testDir",
- givenVer: "3.0.0",
+ "without python 3, python 3 required": {
+ givenDir: "testDir",
+ requiredPy: ver3,
+ init: func(m *mocked) {
+ m.On("LookPath", "python3").Return("", errors.New("")).Once()
+ m.On("LookPath", "py.exe").Return("", errors.New("")).Once()
+ m.On("LookPath", "python3.exe").Return("", errors.New("")).Once()
+ },
+ withError: &ErrRuntimeNotFound,
+ },
+ "without python 2, python 2 required": {
+ givenDir: srcDir,
+ requiredPy: ver2,
+ init: func(m *mocked) {
+ m.On("LookPath", "python2").Return("", errors.New("")).Once()
+ m.On("LookPath", "py.exe").Return("", errors.New("")).Once()
+ m.On("LookPath", "python2.exe").Return("", errors.New("")).Once()
+ },
+ withError: &ErrRuntimeNotFound,
+ },
+ "with python 3.4 and pip, python 3 required": {
+ givenDir: srcDir,
+ veDir: veDir,
+ requiredPy: ver3,
+ goos: "linux",
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
+ m.On("LookPath", "python3").Return(py3Bin, nil).Once()
+ m.On("LookPath", "bash").Return(bashBin, nil).Times(3)
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "--version"},
+ }, true).Return([]byte(py34Version), nil).Twice()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "pip", "--version"},
+ }, true).Return([]byte(py3PipVersion), nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "venv", "--version"},
+ }, true).Return([]byte(py3VenvHelp), nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "ensurepip", "--upgrade"},
+ }, true).Return(nil, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/python3",
- Args: []string{"/test/python3", "--version"},
- }, true).Return([]byte("Python 3.1.0"), nil).Once()
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "pip", "install", "--no-cache", "--upgrade", "pip", "setuptools"},
+ }, true).Return(nil, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3Bin,
+ Args: []string{py3Bin, "-m", "venv", "veDir"},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
+ m.On("GetOS").Return("linux").Times(4)
+ m.On("FileExists", veDir).Return(true, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: bashBin,
+ Args: []string{"source", "veDir/bin/activate"},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
m.On("FileExists", "testDir/requirements.txt").Return(true, nil).Once()
+ m.On("FileExists", ".").Return(true, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/pip3",
- Args: []string{"/test/pip3", "install", "--user", "--ignore-installed", "-r", "requirements.txt"},
- Dir: "testDir",
- }).Return(nil, nil).Once()
+ Path: py3VeBin,
+ Args: []string{py3VeBin, "-m", "pip", "install", "--upgrade", "--ignore-installed", "-r", "testDir/requirements.txt"},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: bashBin,
+ Args: []string{"deactivate"},
+ }, true).Return(nil, nil).Once()
},
},
- "with version 2 and pip": {
- givenDir: "testDir",
- givenVer: "2.0.0",
+ "with py 3.4 (windows) and pip, python 3 required": {
+ givenDir: srcDir,
+ veDir: veDir,
+ requiredPy: ver3,
+ goos: "windows",
init: func(m *mocked) {
- m.On("LookPath", "python2").Return("/test/python2", nil).Once()
- m.On("LookPath", "pip2").Return("/test/pip2", nil).Once()
+ m.On("LookPath", "python3").Return("", errors.New("")).Once()
+ m.On("LookPath", "python3.exe").Return(py3BinWindows, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "-m", "pip", "--version"},
+ }, true).Return([]byte(py3WindowsPipVersion), nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/python2",
- Args: []string{"/test/python2", "--version"},
- }, true).Return([]byte("Python 2.1.0"), nil).Once()
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "--version"},
+ }, true).Return([]byte(py34Version), nil).Twice()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "-m", "venv", "--version"},
+ }, true).Return([]byte(py3VenvHelp), nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "-m", "ensurepip", "--upgrade"},
+ }, true).Return(nil, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "-m", "pip", "install", "--no-cache", "--upgrade", "pip", "setuptools"},
+ }, true).Return(nil, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py3BinWindows,
+ Args: []string{py3BinWindows, "-m", "venv", "veDir"},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
+ m.On("FileExists", veDir).Return(true, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: "veDir/Scripts/activate.bat",
+ Args: []string{},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
+ m.On("GetOS").Return("windows").Times(4)
m.On("FileExists", "testDir/requirements.txt").Return(true, nil).Once()
+ m.On("FileExists", ".").Return(true, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/pip2",
- Args: []string{"/test/pip2", "install", "--user", "--ignore-installed", "-r", "requirements.txt"},
- Dir: "testDir",
- }).Return(nil, nil).Once()
+ Path: winVePipPath,
+ Args: []string{winVePipPath, "install", "--upgrade", "--ignore-installed", "-r", requirementsFile},
+ Dir: "",
+ }, true).Return(nil, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: winDeactivatePath,
+ Args: []string{winDeactivatePath},
+ }, true).Return(nil, nil).Once()
},
},
- "with default version and pip": {
- givenDir: "testDir",
- givenVer: "*",
+ "with python 2, 3.9 and pip, python 2 required": {
+ givenDir: srcDir,
+ requiredPy: ver2,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
- m.On("FileExists", "testDir/requirements.txt").Return(true, nil).Once()
+ m.On("LookPath", "python2").Return(py2Bin, nil).Once()
+ m.On("LookPath", "pip2").Return(pip2Bin, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/pip3",
- Args: []string{"/test/pip3", "install", "--user", "--ignore-installed", "-r", "requirements.txt"},
+ Path: py2Bin,
+ Args: []string{py2Bin, "--version"},
+ }, true).Return([]byte(py2Version), nil).Once()
+ m.On("FileExists", requirementsFile).Return(true, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: pip2Bin,
+ Args: []string{pip2Bin, "install", "--user", "--ignore-installed", "-r", requirementsFile},
Dir: "testDir",
- }).Return(nil, nil).Once()
+ }).Return([]byte(py2Version), nil).Once()
},
},
- "with default version and no pip": {
- givenDir: "testDir",
- givenVer: "*",
+ "with python 2, python 2 required": {
+ givenDir: srcDir,
+ requiredPy: ver2,
+ goos: "linux",
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
- m.On("FileExists", "testDir/requirements.txt").Return(false, nil).Once()
+ m.On("LookPath", "python2").Return(py2Bin, nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py2Bin,
+ Args: []string{py2Bin, "--version"},
+ }, true).Return([]byte(py2Version), nil).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: pip2Bin,
+ Args: []string{pip2Bin, "install", "--user", "--ignore-installed", "-r", requirementsFile},
+ Dir: srcDir,
+ }).Return([]byte(py2Version), nil).Once()
+ m.On("LookPath", "pip2").Return(pip2Bin, nil).Once()
+ m.On("FileExists", requirementsFile).Return(true, nil).Once()
},
},
- "pip exec error": {
- givenDir: "testDir",
- givenVer: "*",
+ "with empty required version, error version not supported": {
+ givenDir: srcDir,
+ veDir: veDir,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
- m.On("FileExists", "testDir/requirements.txt").Return(true, nil).Once()
- m.On("ExecCommand", &exec.Cmd{
- Path: "/test/pip3",
- Args: []string{"/test/pip3", "install", "--user", "--ignore-installed", "-r", "requirements.txt"},
- Dir: "testDir",
- }).Return(nil, &exec.ExitError{}).Once()
},
- withError: ErrPackageManagerExec,
+ withError: &ErrPythonVersionNotSupported,
},
"version not found": {
- givenDir: "testDir",
- givenVer: "3.0.0",
+ givenDir: srcDir,
+ veDir: veDir,
+ requiredPy: ver3,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
+ m.On("LookPath", "python3").Return(py3Bin, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/python3",
- Args: []string{"/test/python3", "--version"},
- }, true).Return([]byte(""), nil).Once()
+ Path: py3Bin,
+ Args: []string{py3Bin, "--version"},
+ }, true).Return([]byte{}, nil).Once()
},
- withError: ErrRuntimeNoVersionFound,
+ withError: &ErrRuntimeNoVersionFound,
},
"version too low": {
- givenDir: "testDir",
- givenVer: "3.0.5",
+ givenDir: srcDir,
+ veDir: veDir,
+ requiredPy: ver355,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("/test/pip3", nil).Once()
+ m.On("LookPath", "python3").Return(py3Bin, nil).Once()
m.On("ExecCommand", &exec.Cmd{
- Path: "/test/python3",
- Args: []string{"/test/python3", "--version"},
- }, true).Return([]byte("Python 3.0.1"), nil).Once()
+ Path: py3Bin,
+ Args: []string{py3Bin, "--version"},
+ }, true).Return([]byte(py34Version), nil).Once()
},
- withError: ErrRuntimeMinimumVersionRequired,
+ withError: &ErrRuntimeMinimumVersionRequired,
},
- "python bin not found": {
- givenDir: "testDir",
- givenVer: "*",
+ "python 2 required, pip2 bin not found": {
+ givenDir: srcDir,
+ requiredPy: ver2,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("", fmt.Errorf("not found")).Once()
- m.On("LookPath", "python2").Return("", fmt.Errorf("not found")).Once()
- m.On("LookPath", "python").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "python2").Return(py2Bin, nil).Once()
+ m.On("LookPath", "pip2").Return("", fmt.Errorf("not found")).Once()
+ m.On("ExecCommand", &exec.Cmd{
+ Path: py2Bin,
+ Args: []string{py2Bin, "--version"},
+ }, true).Return([]byte(py2Version), nil).Once()
},
- withError: ErrRuntimeNotFound,
+ withError: &ErrPackageManagerNotFound,
},
- "pip bin not found": {
- givenDir: "testDir",
- givenVer: "*",
+ "python 2 required, just python 3 is installed": {
+ givenDir: srcDir,
+ requiredPy: ver2,
init: func(m *mocked) {
- m.On("LookPath", "python3").Return("/test/python3", nil).Once()
- m.On("LookPath", "pip3").Return("", fmt.Errorf("not found")).Once()
- m.On("LookPath", "pip2").Return("", fmt.Errorf("not found")).Once()
- m.On("LookPath", "pip").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "python2").Return("", fmt.Errorf("not found")).Once()
+ m.On("LookPath", "python2.exe").Return("", fmt.Errorf("not found")).Once()
+ 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: ErrPackageManagerNotFound,
+ withError: &ErrRuntimeNotFound,
},
}
@@ -145,10 +374,10 @@ func TestInstallPython(t *testing.T) {
m := new(mocked)
test.init(m)
l := langManager{m}
- err := l.installPython(context.Background(), test.givenDir, test.givenVer)
+ 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.True(t, errors.As(err, test.withError), "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 cffd98c..20b7062 100644
--- a/pkg/packages/ruby.go
+++ b/pkg/packages/ruby.go
@@ -48,17 +48,13 @@ func (l *langManager) installRuby(ctx context.Context, dir, cmdReq string) error
return fmt.Errorf("%w: %s:%s", ErrRuntimeNoVersionFound, "ruby", cmdReq)
}
- if version.Compare(cmdReq, matches[1]) == -1 {
+ if version.Compare(cmdReq, matches[1]) == version.Greater {
logger.Debugf("Ruby Version found: %s", matches[1])
return fmt.Errorf("%w: required: %s:%s, have: %s. Please upgrade your runtime", ErrRuntimeMinimumVersionRequired, "ruby", cmdReq, matches[1])
}
}
- if err := installRubyDepsBundler(ctx, l.commandExecutor, dir); err != nil {
- return err
- }
-
- return nil
+ return installRubyDepsBundler(ctx, l.commandExecutor, dir)
}
func installRubyDepsBundler(ctx context.Context, cmdExecutor executor, dir string) error {
diff --git a/pkg/packages/ruby_test.go b/pkg/packages/ruby_test.go
index 825e487..02865b4 100644
--- a/pkg/packages/ruby_test.go
+++ b/pkg/packages/ruby_test.go
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"os/exec"
"testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestInstallRuby(t *testing.T) {
diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go
index bd08105..4b2338c 100644
--- a/pkg/stats/stats.go
+++ b/pkg/stats/stats.go
@@ -17,7 +17,6 @@ package stats
import (
"context"
"fmt"
- "github.com/akamai/cli/pkg/log"
"io/ioutil"
"net/http"
"net/url"
@@ -25,11 +24,11 @@ import (
"strings"
"time"
- "github.com/fatih/color"
- "github.com/google/uuid"
-
"github.com/akamai/cli/pkg/config"
+ "github.com/akamai/cli/pkg/log"
"github.com/akamai/cli/pkg/terminal"
+ "github.com/fatih/color"
+ "github.com/google/uuid"
)
// Akamai CLI (optionally) tracks upgrades, package installs, and updates anonymously
@@ -38,7 +37,7 @@ import (
const (
statsVersion string = "1.1"
- sleepTime24Hours = time.Hour * 24
+ sleep24HDuration = time.Hour * 24
)
// FirstRunCheckStats ...
@@ -238,7 +237,7 @@ func CheckPing(ctx context.Context) error {
}
currentTime := time.Now()
- if lastPing.Add(sleepTime24Hours).Before(currentTime) {
+ if lastPing.Add(sleep24HDuration).Before(currentTime) {
doPing = true
}
}
diff --git a/pkg/stats/stats_test.go b/pkg/stats/stats_test.go
index 08b0ddd..d27fa01 100644
--- a/pkg/stats/stats_test.go
+++ b/pkg/stats/stats_test.go
@@ -3,6 +3,13 @@ package stats
import (
"context"
"fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+
"github.com/akamai/cli/pkg/config"
"github.com/akamai/cli/pkg/terminal"
"github.com/akamai/cli/pkg/version"
@@ -10,12 +17,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "strings"
- "testing"
)
type mocked struct {
diff --git a/pkg/terminal/mock.go b/pkg/terminal/mock.go
index de780fd..fdcbb06 100644
--- a/pkg/terminal/mock.go
+++ b/pkg/terminal/mock.go
@@ -1,8 +1,9 @@
package terminal
import (
- "github.com/stretchr/testify/mock"
"io"
+
+ "github.com/stretchr/testify/mock"
)
// Mock terminal
diff --git a/pkg/terminal/spinner.go b/pkg/terminal/spinner.go
index 7cd7e7e..adb7c4c 100644
--- a/pkg/terminal/spinner.go
+++ b/pkg/terminal/spinner.go
@@ -2,11 +2,12 @@ package terminal
import (
"fmt"
- spnr "github.com/briandowns/spinner"
- "github.com/fatih/color"
"io"
"strings"
"time"
+
+ spnr "github.com/briandowns/spinner"
+ "github.com/fatih/color"
)
type (
diff --git a/pkg/terminal/spinner_test.go b/pkg/terminal/spinner_test.go
index 4b4d7ba..e97529f 100644
--- a/pkg/terminal/spinner_test.go
+++ b/pkg/terminal/spinner_test.go
@@ -3,10 +3,11 @@ package terminal
import (
"bytes"
"fmt"
- spnr "github.com/briandowns/spinner"
- "github.com/stretchr/testify/assert"
"testing"
"time"
+
+ spnr "github.com/briandowns/spinner"
+ "github.com/stretchr/testify/assert"
)
func TestStart(t *testing.T) {
@@ -70,7 +71,7 @@ func TestOK(t *testing.T) {
}
s.Start("spinner %s", "test")
s.OK()
- assert.Contains(t, wr.String(), fmt.Sprintf("spinner test ... [OK]"))
+ assert.Contains(t, wr.String(), "spinner test ... [OK]")
}
func TestWarn(t *testing.T) {
@@ -80,7 +81,7 @@ func TestWarn(t *testing.T) {
}
s.Start("spinner %s", "test")
s.Warn()
- assert.Contains(t, wr.String(), fmt.Sprintf("spinner test ... [WARN]"))
+ assert.Contains(t, wr.String(), "spinner test ... [WARN]")
}
func TestWarnOK(t *testing.T) {
@@ -90,7 +91,7 @@ func TestWarnOK(t *testing.T) {
}
s.Start("spinner %s", "test")
s.WarnOK()
- assert.Contains(t, wr.String(), fmt.Sprintf("spinner test ... [OK]"))
+ assert.Contains(t, wr.String(), "spinner test ... [OK]")
}
func TestFail(t *testing.T) {
diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go
index b901783..bbae6c7 100644
--- a/pkg/terminal/terminal.go
+++ b/pkg/terminal/terminal.go
@@ -19,18 +19,15 @@ import (
"errors"
"fmt"
"io"
+ "os"
"strings"
"time"
- "os"
-
+ "github.com/AlecAivazis/survey/v2"
"github.com/akamai/cli/pkg/version"
"github.com/fatih/color"
-
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
-
- "github.com/AlecAivazis/survey/v2"
)
type (
diff --git a/pkg/terminal/terminal_test.go b/pkg/terminal/terminal_test.go
index 0481642..e98af58 100644
--- a/pkg/terminal/terminal_test.go
+++ b/pkg/terminal/terminal_test.go
@@ -16,11 +16,12 @@ package terminal
import (
"context"
- "github.com/stretchr/testify/require"
- "github.com/tj/assert"
"io/ioutil"
"os"
"testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/tj/assert"
)
func TestWrite(t *testing.T) {
diff --git a/pkg/tools/util.go b/pkg/tools/util.go
index 472449a..f205af4 100644
--- a/pkg/tools/util.go
+++ b/pkg/tools/util.go
@@ -28,7 +28,13 @@ func Self() string {
return filepath.Base(os.Args[0])
}
-// GetAkamaiCliPath ...
+// GetAkamaiCliPath returns the "$AKAMAI_CLI_HOME/.akamai-cli" value and tries to create it if not existing.
+//
+// Errors out if:
+//
+// * $AKAMAI_CLI_HOME is not defined
+//
+// * $AKAMAI_CLI_HOME/.akamai-cli does not exist, and we cannot create it
func GetAkamaiCliPath() (string, error) {
cliHome := os.Getenv("AKAMAI_CLI_HOME")
if cliHome == "" {
@@ -48,14 +54,37 @@ func GetAkamaiCliPath() (string, error) {
return cliPath, nil
}
-// GetAkamaiCliSrcPath ...
+// GetAkamaiCliSrcPath returns $AKAMAI_CLI_HOME/.akamai-cli/src
func GetAkamaiCliSrcPath() (string, error) {
- cliHome, _ := GetAkamaiCliPath()
+ cliHome, err := GetAkamaiCliPath()
+ if err != nil {
+ return "", err
+ }
return filepath.Join(cliHome, "src"), nil
}
-// Githubize ..
+// GetAkamaiCliVenvPath - returns the .akamai-cli/venv path, for Python virtualenv
+func GetAkamaiCliVenvPath() (string, error) {
+ cliHome, err := GetAkamaiCliPath()
+ if err != nil {
+ return "", err
+ }
+
+ return filepath.Join(cliHome, "venv"), nil
+}
+
+// GetPkgVenvPath - returns the package virtualenv path
+func GetPkgVenvPath(pkgName string) (string, error) {
+ vePath, err := GetAkamaiCliVenvPath()
+ if err != nil {
+ return "", err
+ }
+
+ return filepath.Join(vePath, pkgName), nil
+}
+
+// Githubize returns the GitHub package repository URI
func Githubize(repo string) string {
if strings.HasPrefix(repo, "http") || strings.HasPrefix(repo, "ssh") || strings.HasSuffix(repo, ".git") {
return strings.TrimPrefix(repo, "ssh://")
diff --git a/pkg/tools/util_test.go b/pkg/tools/util_test.go
index cbbd39f..d657f26 100644
--- a/pkg/tools/util_test.go
+++ b/pkg/tools/util_test.go
@@ -26,20 +26,20 @@ func TestVersionCompare(t *testing.T) {
right string
result int
}{
- {"0.9.9", "1.0.0", 1},
- {"0.1.0", "0.2.0", 1},
- {"0.3.0", "0.3.1", 1},
- {"0.1.0", "0.1.0", 0},
- {"1.0.0", "0.9.9", -1},
- {"0.2.0", "0.1.0", -1},
- {"0.3.1", "0.3.0", -1},
- {"1", "2", 1},
- {"1.1", "1.2", 1},
- {"3.0.0", "3.1.4", 1},
- {"1.1.0", "1.1.1", 1},
- {"1.1.0", "1.1.1-dev", 1},
- {"1.0.4", "1.1.1-dev", 1},
- {"1.1.3", "1.1.4-dev", 1},
+ {"0.9.9", "1.0.0", version.Smaller},
+ {"0.1.0", "0.2.0", version.Smaller},
+ {"0.3.0", "0.3.1", version.Smaller},
+ {"0.1.0", "0.1.0", version.Equals},
+ {"1.0.0", "0.9.9", version.Greater},
+ {"0.2.0", "0.1.0", version.Greater},
+ {"0.3.1", "0.3.0", version.Greater},
+ {"1", "2", version.Smaller},
+ {"1.1", "1.2", version.Smaller},
+ {"3.0.0", "3.1.4", version.Smaller},
+ {"1.1.0", "1.1.1", version.Smaller},
+ {"1.1.0", "1.1.1-dev", version.Smaller},
+ {"1.0.4", "1.1.1-dev", version.Smaller},
+ {"1.1.3", "1.1.4-dev", version.Smaller},
}
for _, tt := range versionTests {
diff --git a/pkg/version/version.go b/pkg/version/version.go
index dbd0bf4..cd97d00 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -4,26 +4,42 @@ import "github.com/Masterminds/semver"
const (
// Version Application Version
- Version = "1.3.1"
+ Version = "1.4.0"
+ // Equals p1==p2 in version.Compare(p1, p2)
+ Equals = 0
+ // Error failure parsing one of the parameters in version.Compare(p1, p2)
+ Error = 2
+ // Greater p1>p2 in version.Compare(p1, p2)
+ Greater = -1
+ // Smaller p1 right: return -1 (version.Greater)
+//
+// * if left == right: return 0 (version.Equals)
+//
+// * if unable to parse left or right: return 2 (version.Error)
func Compare(left, right string) int {
leftVersion, err := semver.NewVersion(left)
if err != nil {
- return -2
+ return Error
}
rightVersion, err := semver.NewVersion(right)
if err != nil {
- return 2
+ return Error
}
if leftVersion.LessThan(rightVersion) {
- return 1
+ return Smaller
} else if leftVersion.GreaterThan(rightVersion) {
- return -1
+ return Greater
}
- return 0
+ return Equals
}
diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go
index 877278a..039b3df 100644
--- a/pkg/version/version_test.go
+++ b/pkg/version/version_test.go
@@ -1,8 +1,9 @@
package version
import (
- "github.com/tj/assert"
"testing"
+
+ "github.com/tj/assert"
)
func TestCompareVersion(t *testing.T) {
@@ -10,11 +11,11 @@ func TestCompareVersion(t *testing.T) {
left, right string
expected int
}{
- "left is greater than right": {"1.0.1", "1.0.0", -1},
- "left is less than right": {"0.9.0", "1.0.0", 1},
- "versions are equal": {"0.9.0", "0.9.0", 0},
- "left version does not match semver syntax": {"abc", "0.9.0", -2},
- "right version does not match semver syntax": {"1.0.0", "abc", 2},
+ "left is greater than right": {"1.0.1", "1.0.0", Greater},
+ "left is less than right": {"0.9.0", "1.0.0", Smaller},
+ "versions are equal": {"0.9.0", "0.9.0", Equals},
+ "left version does not match semver syntax": {"abc", "0.9.0", Error},
+ "right version does not match semver syntax": {"1.0.0", "abc", Error},
}
for name, test := range tests {