From ab7a5a0504e052aa6fc3b2e379a7f7ae857b495c Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 6 Jun 2024 11:43:29 -0400 Subject: [PATCH 1/2] add go-install arguments and env vars Signed-off-by: Alex Goodman --- README.md | 50 ++++++++++++------------ test/cli/trait_assertions_test.go | 2 +- tool/goinstall/installer.go | 63 ++++++++++++++++++++++++++----- tool/goinstall/installer_test.go | 14 ++++++- 4 files changed, 93 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 17bc118..041f61f 100644 --- a/README.md +++ b/README.md @@ -84,15 +84,15 @@ with: # arbitrary key-value pairs for the install method ``` -| Option | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | The name of the tool to install. This is used to determine the installation directory and the name of the binary. | -| `version.want` | The version of the tool to install. This can be a specific version, or a version range. | -| `version.constraint` | A constraint on the version of the tool to install. This is used to determine the latest version of the tool to update to. | -| `version.method` | The method to use to determine the latest version of the tool. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | -| `version.with` | The configuration options for the version method. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | -| `method` | The method to use to install the tool. See the [Install Methods](#install-methods) section for more details. | -| `with` | The configuration options for the install method. See the [Install Methods](#install-methods) section for more details. | +| Option | Description | +|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | The name of the tool to install. This is used to determine the installation directory and the name of the binary. | +| `version.want` | The version of the tool to install. This can be a specific version, or a version range. | +| `version.constraint` | A constraint on the version of the tool to install. This is used to determine the latest version of the tool to update to. | +| `version.method` | The method to use to determine the latest version of the tool. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `version.with` | The configuration options for the version method. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `method` | The method to use to install the tool. See the [Install Methods](#install-methods) section for more details. | +| `with` | The configuration options for the install method. See the [Install Methods](#install-methods) section for more details. | ### Install Methods @@ -115,20 +115,22 @@ The default version resolver for this method is `github-release`. The `go-install` install method uses `go install` to install a tool. It requires the following configuration options: -| Option | Description | -|-------------------------|-----------------------------------------------------------------------------| -| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | -| `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | -| `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | +| Option | Description | +|-------------------------|--------------------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | +| `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | +| `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | +| `args` (optional) | A list of args/flags to pass to `go install` (e.g. `-tags containers_image_openpgp`) | +| `env` (optional) | A map environment variables to use when running `go install` | The `module` option allows for a special entry: - `.` or `path/to/module/on/disk` The `ldflags` allow for templating with the following variables: -| Variable | Description | -|--------|--------------------------------------------------------------------------------------------| -| `{{ .Version }}` | The resolved version of the tool (which may differe from that of the `version.want` value) | +| Variable | Description | +|--------|-------------------------------------------------------------------------------------------------------| +| `{{ .Version }}` | The resolved version of the tool (which may differe from that of the `version.want` value) | In addition to these variables, [sprig functions](http://masterminds.github.io/sprig/) are allowed; for example: ```yaml @@ -145,10 +147,10 @@ The default version resolver for this method is `go-proxy`. The `hosted-shell` install method uses a hosted shell script to install a tool. It requires the following configuration options: -| Option | Description | -|--------|------------------------------------------------------------| -| `url` | The URL to the hosted shell script (e.g. `https://raw.githubusercontent.com/anchore/syft/main/install.sh`) | -| `args` (optional) | Arguments to pass to the shell script (as a single string) | +| Option | Description | +|--------|------------------------------------------------------------------------------------------------------------| +| `url` | The URL to the hosted shell script (e.g. `https://raw.githubusercontent.com/anchore/syft/main/install.sh`) | +| `args` (optional) | Arguments to pass to the shell script (as a single string) | If the URL refers to either `github.com` or `raw.githubusercontent.com` then the default version resolver is `github-release`. Otherwise, the version resolver must be specified manually. @@ -208,9 +210,9 @@ is a version constraint used. The `go-proxy` version method reaches out to `proxy.golang.org` to determine the latest version of a Go module. It requires the following configuration options: -| Option | Description | -|--------|----------------------------------------------------------------------------------------------------------------------| -| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | +| Option | Description | +|--------|------------------------------------------------------------------------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | | `allow-unresolved-version` | If the latest version cannot be found by the proxy allow for "latest" as a valid value (which `go install` supports) | The `version.want` option allows a special entry: diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index 61c9188..54eb698 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -2,7 +2,6 @@ package cli import ( "encoding/json" - "github.com/google/go-cmp/cmp" "os" "os/exec" "path/filepath" @@ -11,6 +10,7 @@ import ( "testing" "github.com/acarl005/stripansi" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" ) diff --git a/tool/goinstall/installer.go b/tool/goinstall/installer.go index 7863323..64b0321 100644 --- a/tool/goinstall/installer.go +++ b/tool/goinstall/installer.go @@ -18,14 +18,16 @@ import ( var _ binny.Installer = (*Installer)(nil) type InstallerParameters struct { - Module string `json:"module" yaml:"module" mapstructure:"module"` - Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` - LDFlags []string `json:"ldflags" yaml:"ldflags" mapstructure:"ldflags"` + Module string `json:"module" yaml:"module" mapstructure:"module"` + Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` + LDFlags []string `json:"ldflags" yaml:"ldflags" mapstructure:"ldflags"` + Args []string `json:"args" yaml:"args" mapstructure:"args"` + Env map[string]any `json:"env" yaml:"env" mapstructure:"env"` } type Installer struct { config InstallerParameters - goInstallRunner func(spec, ldflags, destDir string) error + goInstallRunner func(spec, ldflags string, args []string, env []string, destDir string) error } func NewInstaller(cfg InstallerParameters) Installer { @@ -57,17 +59,44 @@ func (i Installer) InstallTo(version, destDir string) (string, error) { return "", fmt.Errorf("failed to template ldflags: %v", err) } - if err := i.goInstallRunner(spec, ldflags, destDir); err != nil { + args, err := templateSlice(i.config.Args, version) + if err != nil { + return "", fmt.Errorf("failed to template args: %v", err) + } + + env, err := templateSlice(toEnvSlice(i.config.Env), version) + if err != nil { + return "", fmt.Errorf("failed to template env: %v", err) + } + + if err := i.goInstallRunner(spec, ldflags, args, env, destDir); err != nil { return "", fmt.Errorf("failed to install: %v", err) } return binPath, nil } +func templateSlice(in []string, version string) ([]string, error) { + ret := make([]string, len(in)) + for i, arg := range in { + rendered, err := templateString(arg, version) + if err != nil { + return nil, err + } + + ret[i] = rendered + } + return ret, nil +} + func templateFlags(ldFlags []string, version string) (string, error) { flags := strings.Join(ldFlags, " ") - tmpl, err := template.New("ldflags").Funcs(sprig.FuncMap()).Parse(flags) + return templateString(flags, version) +} + +func templateString(in string, version string) (string, error) { + tmpl, err := template.New("ldflags").Funcs(sprig.FuncMap()).Parse(in) if err != nil { return "", err } @@ -84,17 +113,25 @@ func templateFlags(ldFlags []string, version string) (string, error) { return buf.String(), nil } -func runGoInstall(spec, ldflags, destDir string) error { +func runGoInstall(spec, ldflags string, userArgs, userEnv []string, destDir string) error { args := []string{"install"} + args = append(args, userArgs...) + if ldflags != "" { args = append(args, fmt.Sprintf("-ldflags=%s", ldflags)) } args = append(args, spec) - log.Trace("running: go " + strings.Join(args, " ")) + log.WithFields("env-vars", len(userEnv)).Trace("running: go " + strings.Join(args, " ")) cmd := exec.Command("go", args...) - cmd.Env = append(os.Environ(), "GOBIN="+destDir) + + // set env vars... + env := os.Environ() + env = append(env, userEnv...) + // always override any conflicting env vars + env = append(env, "GOBIN="+destDir) + cmd.Env = env output, err := cmd.CombinedOutput() if err != nil { @@ -102,3 +139,11 @@ func runGoInstall(spec, ldflags, destDir string) error { } return nil } + +func toEnvSlice(env map[string]any) []string { + var envSlice []string + for k, v := range env { + envSlice = append(envSlice, fmt.Sprintf("%s=%v", k, v)) + } + return envSlice +} diff --git a/tool/goinstall/installer_test.go b/tool/goinstall/installer_test.go index 689423b..4929c46 100644 --- a/tool/goinstall/installer_test.go +++ b/tool/goinstall/installer_test.go @@ -66,7 +66,7 @@ func Test_templateFlags(t *testing.T) { func TestInstaller_InstallTo(t *testing.T) { type fields struct { config InstallerParameters - goInstallRunner func(spec, ldflags, destDir string) error + goInstallRunner func(spec, ldflags string, args, env []string, destDir string) error } type args struct { version string @@ -88,11 +88,21 @@ func TestInstaller_InstallTo(t *testing.T) { LDFlags: []string{ "-X github.com/anchore/binny/internal/version.Version={{.Version}}", }, + Args: []string{ + "-tags", + "containers_image_openpgp", + }, + Env: map[string]any{ + "FOO": "BAR", + "BAZ": 0, + }, }, - goInstallRunner: func(spec, ldflags, destDir string) error { + goInstallRunner: func(spec, ldflags string, userArgs, userEnv []string, destDir string) error { assert.Equal(t, "github.com/anchore/binny/cmd/binny@1.2.3", spec) assert.Equal(t, "-X github.com/anchore/binny/internal/version.Version=1.2.3", ldflags) assert.Equal(t, "/tmp/to/place", destDir) + assert.Equal(t, []string{"-tags", "containers_image_openpgp"}, userArgs) + assert.Equal(t, []string{"FOO=BAR", "BAZ=0"}, userEnv) return nil }, }, From 241dae59530fd56b03e8279bcc77f8bd6168a835 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 6 Jun 2024 14:26:37 -0400 Subject: [PATCH 2/2] use slice for env var + create store dir if not exists Signed-off-by: Alex Goodman --- README.md | 2 +- cmd/binny/cli/command/add_go_install.go | 15 +++++++++++ cmd/binny/cli/option/go_install.go | 10 +++++--- tool/goinstall/installer.go | 33 ++++++++++++++----------- tool/goinstall/installer_test.go | 6 ++--- tool/install.go | 7 ++++++ 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 041f61f..a9abb36 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ The `go-install` install method uses `go install` to install a tool. It requires | `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | | `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | | `args` (optional) | A list of args/flags to pass to `go install` (e.g. `-tags containers_image_openpgp`) | -| `env` (optional) | A map environment variables to use when running `go install` | +| `env` (optional) | A list key=value environment variables to use when running `go install` | The `module` option allows for a special entry: - `.` or `path/to/module/on/disk` diff --git a/cmd/binny/cli/command/add_go_install.go b/cmd/binny/cli/command/add_go_install.go index 1457eae..011dd8c 100644 --- a/cmd/binny/cli/command/add_go_install.go +++ b/cmd/binny/cli/command/add_go_install.go @@ -75,10 +75,16 @@ func runAddGoInstallConfig(cmdCfg AddGoInstallConfig, nameVersion string) error return fmt.Errorf("invalid ldflags: %w", err) } + if err := validateEnvSlice(iCfg.Env); err != nil { + return err + } + coreInstallParams := goinstall.InstallerParameters{ Module: iCfg.Module, Entrypoint: iCfg.Entrypoint, LDFlags: ldFlagsList, + Args: iCfg.Args, + Env: iCfg.Env, } installParamMap, err := toMap(coreInstallParams) @@ -103,3 +109,12 @@ func runAddGoInstallConfig(cmdCfg AddGoInstallConfig, nameVersion string) error return updateConfiguration(cmdCfg.Config, toolCfg) } + +func validateEnvSlice(env []string) error { + for _, e := range env { + if !strings.Contains(e, "=") { + return fmt.Errorf("invalid env format: %q", e) + } + } + return nil +} diff --git a/cmd/binny/cli/option/go_install.go b/cmd/binny/cli/option/go_install.go index 025b0fd..edef733 100644 --- a/cmd/binny/cli/option/go_install.go +++ b/cmd/binny/cli/option/go_install.go @@ -3,13 +3,17 @@ package option import "github.com/anchore/clio" type GoInstall struct { - Module string `json:"module" yaml:"module" mapstructure:"module"` - Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` - LDFlags string `json:"ld-flags" yaml:"ld-flags" mapstructure:"ld-flags"` + Module string `json:"module" yaml:"module" mapstructure:"module"` + Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` + LDFlags string `json:"ld-flags" yaml:"ld-flags" mapstructure:"ld-flags"` + Args []string `json:"args" yaml:"args" mapstructure:"args"` + Env []string `json:"env" yaml:"env" mapstructure:"env"` } func (o *GoInstall) AddFlags(flags clio.FlagSet) { flags.StringVarP(&o.Module, "module", "m", "Go module (e.g. github.com/anchore/syft)") flags.StringVarP(&o.Entrypoint, "entrypoint", "e", "Entrypoint within the go module (e.g. cmd/syft)") flags.StringVarP(&o.LDFlags, "ld-flags", "l", "LD flags to pass to the go install command (e.g. -ldflags \"-X main.version=1.0.0\")") + flags.StringArrayVarP(&o.Args, "args", "a", "Additional arguments to pass to the go install command") + flags.StringArrayVarP(&o.Env, "env", "", "Environment variables to pass to the go install command") } diff --git a/tool/goinstall/installer.go b/tool/goinstall/installer.go index 64b0321..7e69288 100644 --- a/tool/goinstall/installer.go +++ b/tool/goinstall/installer.go @@ -18,11 +18,11 @@ import ( var _ binny.Installer = (*Installer)(nil) type InstallerParameters struct { - Module string `json:"module" yaml:"module" mapstructure:"module"` - Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` - LDFlags []string `json:"ldflags" yaml:"ldflags" mapstructure:"ldflags"` - Args []string `json:"args" yaml:"args" mapstructure:"args"` - Env map[string]any `json:"env" yaml:"env" mapstructure:"env"` + Module string `json:"module" yaml:"module" mapstructure:"module"` + Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` + LDFlags []string `json:"ldflags" yaml:"ldflags" mapstructure:"ldflags"` + Args []string `json:"args" yaml:"args" mapstructure:"args"` + Env []string `json:"env" yaml:"env" mapstructure:"env"` } type Installer struct { @@ -64,7 +64,11 @@ func (i Installer) InstallTo(version, destDir string) (string, error) { return "", fmt.Errorf("failed to template args: %v", err) } - env, err := templateSlice(toEnvSlice(i.config.Env), version) + if err := validateEnvSlice(i.config.Env); err != nil { + return "", err + } + + env, err := templateSlice(i.config.Env, version) if err != nil { return "", fmt.Errorf("failed to template env: %v", err) } @@ -76,6 +80,15 @@ func (i Installer) InstallTo(version, destDir string) (string, error) { return binPath, nil } +func validateEnvSlice(env []string) error { + for _, e := range env { + if !strings.Contains(e, "=") { + return fmt.Errorf("invalid env format: %q", e) + } + } + return nil +} + func templateSlice(in []string, version string) ([]string, error) { ret := make([]string, len(in)) for i, arg := range in { @@ -139,11 +152,3 @@ func runGoInstall(spec, ldflags string, userArgs, userEnv []string, destDir stri } return nil } - -func toEnvSlice(env map[string]any) []string { - var envSlice []string - for k, v := range env { - envSlice = append(envSlice, fmt.Sprintf("%s=%v", k, v)) - } - return envSlice -} diff --git a/tool/goinstall/installer_test.go b/tool/goinstall/installer_test.go index 4929c46..cf79b7c 100644 --- a/tool/goinstall/installer_test.go +++ b/tool/goinstall/installer_test.go @@ -92,9 +92,9 @@ func TestInstaller_InstallTo(t *testing.T) { "-tags", "containers_image_openpgp", }, - Env: map[string]any{ - "FOO": "BAR", - "BAZ": 0, + Env: []string{ + "FOO=BAR", + "BAZ=0", }, }, goInstallRunner: func(spec, ldflags string, userArgs, userEnv []string, destDir string) error { diff --git a/tool/install.go b/tool/install.go index c6c4457..60d3371 100644 --- a/tool/install.go +++ b/tool/install.go @@ -91,6 +91,13 @@ func makeStagingArea(store *binny.Store, toolName string) (string, func(), error if err != nil { return "", cleanup, fmt.Errorf("failed to get absolute path for store root: %w", err) } + + if _, err := os.Stat(absRoot); os.IsNotExist(err) { + if err := os.MkdirAll(absRoot, 0755); err != nil { + return "", cleanup, fmt.Errorf("failed to create store root: %w", err) + } + } + tmpdir, err := os.MkdirTemp(absRoot, fmt.Sprintf("binny-install-%s-", toolName)) if err != nil { return "", cleanup, fmt.Errorf("failed to create temp directory: %w", err)