Skip to content

Commit

Permalink
Add go-install arguments and env vars (#23)
Browse files Browse the repository at this point in the history
* add go-install arguments and env vars

Signed-off-by: Alex Goodman <[email protected]>

* use slice for env var + create store dir if not exists

Signed-off-by: Alex Goodman <[email protected]>

---------

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Jun 6, 2024
1 parent 6d4d072 commit 359e2f7
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 36 deletions.
50 changes: 26 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 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`

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
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions cmd/binny/cli/command/add_go_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
10 changes: 7 additions & 3 deletions cmd/binny/cli/option/go_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
2 changes: 1 addition & 1 deletion test/cli/trait_assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"encoding/json"
"github.com/google/go-cmp/cmp"
"os"
"os/exec"
"path/filepath"
Expand All @@ -11,6 +10,7 @@ import (
"testing"

"github.com/acarl005/stripansi"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

Expand Down
62 changes: 56 additions & 6 deletions tool/goinstall/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ 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 []string `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 {
Expand Down Expand Up @@ -57,17 +59,57 @@ 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)
}

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)
}

if err := i.goInstallRunner(spec, ldflags, args, env, destDir); err != nil {
return "", fmt.Errorf("failed to install: %v", err)
}

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 {
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
}
Expand All @@ -84,17 +126,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 {
Expand Down
14 changes: 12 additions & 2 deletions tool/goinstall/installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: []string{
"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/[email protected]", 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
},
},
Expand Down
7 changes: 7 additions & 0 deletions tool/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 359e2f7

Please sign in to comment.