Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add go-install arguments and env vars #23

Merged
merged 2 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/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
},
},
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
Loading