diff --git a/.gitignore b/.gitignore index 7613027..da20145 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ tmp testdata/caches cache +out.yaml diff --git a/cmd/dependabot/internal/cmd/update.go b/cmd/dependabot/internal/cmd/update.go index a98d124..26e2bc4 100644 --- a/cmd/dependabot/internal/cmd/update.go +++ b/cmd/dependabot/internal/cmd/update.go @@ -160,10 +160,52 @@ func processInput(input *model.Input) { job.SecurityAdvisories = []model.Advisory{} } - // Process environment variables in the scenario file + // As a convenience, fill in a git_source if credentials are in the environment and a git_source + // doesn't already exist. This way the user doesn't run out of calls from being anonymous. + hasLocalToken := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN") != "" + var isGitSourceInCreds bool for _, cred := range input.Credentials { - for key, value := range cred { - cred[key] = os.ExpandEnv(value) + if cred["type"] == "git_source" { + isGitSourceInCreds = true + break + } + } + if hasLocalToken && !isGitSourceInCreds { + log.Println("Inserting $LOCAL_GITHUB_ACCESS_TOKEN into credentials") + input.Credentials = append(input.Credentials, model.Credential{ + "type": "git_source", + "host": "github.com", + "username": "x-access-token", + "password": "$LOCAL_GITHUB_ACCESS_TOKEN", + }) + if len(input.Job.CredentialsMetadata) > 0 { + // Add the metadata since the next section will be skipped. + input.Job.CredentialsMetadata = append(input.Job.CredentialsMetadata, map[string]any{ + "type": "git_source", + "host": "github.com", + }) + } + } + + // As a convenience, fill credentials-metadata if credentials are provided + // which is what happens in production. This way the user doesn't have to + // specify credentials-metadata in the scenario file unless they want to. + if len(input.Job.CredentialsMetadata) == 0 { + log.Println("Adding missing credentials-metadata into job definition") + for _, credential := range input.Credentials { + entry := map[string]any{ + "type": credential["type"], + } + if credential["host"] != nil { + entry["host"] = credential["host"] + } + if credential["url"] != nil { + entry["url"] = credential["url"] + } + if credential["replaces-base"] != nil { + entry["replaces-base"] = credential["replaces-base"] + } + input.Job.CredentialsMetadata = append(input.Job.CredentialsMetadata, entry) } } } diff --git a/cmd/dependabot/internal/cmd/update_test.go b/cmd/dependabot/internal/cmd/update_test.go new file mode 100644 index 0000000..d529f2f --- /dev/null +++ b/cmd/dependabot/internal/cmd/update_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "os" + "reflect" + "testing" + + "github.com/dependabot/cli/internal/model" +) + +func Test_processInput(t *testing.T) { + t.Run("initializes some fields", func(t *testing.T) { + os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "") + + var input model.Input + processInput(&input) + + if input.Job.ExistingPullRequests == nil { + t.Error("expected existing pull requests to be initialized") + } + if input.Job.IgnoreConditions == nil { + t.Error("expected ignore conditions to be initialized") + } + if input.Job.SecurityAdvisories == nil { + t.Error("expected security advisories to be initialized") + } + if len(input.Credentials) != 0 { + t.Fatal("expected NO credentials to be added") + } + }) + + t.Run("adds git_source to credentials when local token is present", func(t *testing.T) { + var input model.Input + os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "token") + // Adding a dummy metadata to test the inner if + input.Job.CredentialsMetadata = []model.Credential{{}} + + processInput(&input) + + if len(input.Credentials) != 1 { + t.Fatal("expected credentials to be added") + } + if !reflect.DeepEqual(input.Credentials[0], model.Credential{ + "type": "git_source", + "host": "github.com", + "username": "x-access-token", + "password": "$LOCAL_GITHUB_ACCESS_TOKEN", + }) { + t.Error("expected credentials to be added") + } + if !reflect.DeepEqual(input.Job.CredentialsMetadata[1], model.Credential{ + "type": "git_source", + "host": "github.com", + }) { + t.Error("expected credentials metadata to be added") + } + }) +} diff --git a/go.mod b/go.mod index e8d929a..0a4e7cb 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.0 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect - golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect gotest.tools/v3 v3.3.0 // indirect ) diff --git a/go.sum b/go.sum index 468e41c..8fc506e 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= diff --git a/internal/infra/config.go b/internal/infra/config.go index fed57f7..2d75689 100644 --- a/internal/infra/config.go +++ b/internal/infra/config.go @@ -1,11 +1,13 @@ package infra +import "github.com/dependabot/cli/internal/model" + // ConfigFilePath is the path to proxy config file. const ConfigFilePath = "/config.json" // Config is the structure of the proxy's config file type Config struct { - Credentials []map[string]string `json:"all_credentials"` + Credentials []model.Credential `json:"all_credentials"` CA CertificateAuthority `json:"ca"` } diff --git a/internal/infra/run.go b/internal/infra/run.go index 6f59773..dd9a6e7 100644 --- a/internal/infra/run.go +++ b/internal/infra/run.go @@ -25,7 +25,7 @@ type RunParams struct { // expectations asserted at the end of a test Expected []model.Output // credentials passed to the proxy - Creds []map[string]string + Creds []model.Credential // local directory used for caching CacheDir string // write output to a file @@ -63,16 +63,6 @@ func Run(params RunParams) error { api := server.NewAPI(params.Expected) defer api.Stop() - token := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN") - if token != "" { - params.Creds = append(params.Creds, map[string]string{ - "type": "git_source", - "host": "github.com", - "username": "x-access-token", - "password": token, - }) - } - var outFile *os.File if params.Output != "" { var err error @@ -85,6 +75,8 @@ func Run(params RunParams) error { defer outFile.Close() } + expandEnvironmentVariables(api, ¶ms) + if err := runContainers(ctx, params, api); err != nil { return err } @@ -124,6 +116,29 @@ func Run(params RunParams) error { return nil } +func expandEnvironmentVariables(api *server.API, params *RunParams) { + api.Actual.Input.Credentials = params.Creds + + // Make a copy of the credentials, so we don't inject them into the output file. + params.Creds = []model.Credential{} + for _, cred := range api.Actual.Input.Credentials { + newCred := model.Credential{} + for k, v := range cred { + newCred[k] = v + } + params.Creds = append(params.Creds, newCred) + } + + // Add the actual credentials from the environment. + for _, cred := range params.Creds { + for key, value := range cred { + if valueString, ok := value.(string); ok { + cred[key] = os.ExpandEnv(valueString) + } + } + } +} + func generateIgnoreConditions(params *RunParams, actual *model.Scenario) error { for _, out := range actual.Output { if out.Type == "create_pull_request" { diff --git a/internal/infra/run_test.go b/internal/infra/run_test.go index 3f533f2..d665f21 100644 --- a/internal/infra/run_test.go +++ b/internal/infra/run_test.go @@ -5,14 +5,50 @@ import ( "bytes" "context" "io" + "os" "reflect" "testing" + "github.com/dependabot/cli/internal/server" + + "gopkg.in/yaml.v3" + "github.com/dependabot/cli/internal/model" "github.com/docker/docker/api/types" "github.com/moby/moby/client" ) +func Test_expandEnvironmentVariables(t *testing.T) { + t.Run("injects environment variables", func(t *testing.T) { + os.Setenv("ENV1", "value1") + os.Setenv("ENV2", "value2") + api := &server.API{} + params := &RunParams{ + Creds: []model.Credential{{ + "type": "test", + "url": "url", + "username": "$ENV1", + "pass": "$ENV2", + }}, + } + + expandEnvironmentVariables(api, params) + + if params.Creds[0]["username"] != "value1" { + t.Error("expected username to be injected", params.Creds[0]["username"]) + } + if params.Creds[0]["pass"] != "value2" { + t.Error("expected pass to be injected", params.Creds[0]["pass"]) + } + if api.Actual.Input.Credentials[0]["username"] != "$ENV1" { + t.Error("expected username NOT to be injected", api.Actual.Input.Credentials[0]["username"]) + } + if api.Actual.Input.Credentials[0]["pass"] != "$ENV2" { + t.Error("expected pass NOT to be injected", api.Actual.Input.Credentials[0]["pass"]) + } + }) +} + func Test_generateIgnoreConditions(t *testing.T) { const ( outputFileName = "test_output" @@ -107,6 +143,14 @@ func TestRun(t *testing.T) { _, _ = cli.ImageRemove(ctx, UpdaterImageName, types.ImageRemoveOptions{}) }() + cred := model.Credential{ + "type": "git_source", + "host": "github.com", + "username": "x-access-token", + "password": "$LOCAL_GITHUB_ACCESS_TOKEN", + } + + os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "test-token") err = Run(RunParams{ PullImages: true, Job: &model.Job{ @@ -115,10 +159,30 @@ func TestRun(t *testing.T) { Repo: "org/name", }, }, + Creds: []model.Credential{cred}, + Output: "out.yaml", }) if err != nil { t.Error(err) } + + f, err := os.Open("out.yaml") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + var output model.Scenario + if err = yaml.NewDecoder(f).Decode(&output); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(output.Input.Credentials, []model.Credential{cred}) { + t.Error("unexpected credentials", output.Input.Credentials) + } + if output.Input.Credentials[0]["password"] != "$LOCAL_GITHUB_ACCESS_TOKEN" { + t.Error("expected password to be masked") + } } const dockerFile = ` diff --git a/internal/model/job.go b/internal/model/job.go index 2e76c3b..879b291 100644 --- a/internal/model/job.go +++ b/internal/model/job.go @@ -2,24 +2,24 @@ package model // Job is the data that is passed to the updater. type Job struct { - PackageManager string `json:"package-manager" yaml:"package-manager"` - AllowedUpdates []Allowed `json:"allowed-updates" yaml:"allowed-updates,omitempty"` - Dependencies []string `json:"dependencies" yaml:"dependencies,omitempty"` - ExistingPullRequests [][]ExistingPR `json:"existing-pull-requests" yaml:"existing-pull-requests,omitempty"` - Experiments Experiment `json:"experiments" yaml:"experiments,omitempty"` - IgnoreConditions []Condition `json:"ignore-conditions" yaml:"ignore-conditions,omitempty"` - LockfileOnly bool `json:"lockfile-only" yaml:"lockfile-only,omitempty"` - RequirementsUpdateStrategy *string `json:"requirements-update-strategy" yaml:"requirements-update-strategy,omitempty"` - SecurityAdvisories []Advisory `json:"security-advisories" yaml:"security-advisories,omitempty"` - SecurityUpdatesOnly bool `json:"security-updates-only" yaml:"security-updates-only,omitempty"` - Source Source `json:"source" yaml:"source"` - UpdateSubdependencies bool `json:"update-subdependencies" yaml:"update-subdependencies,omitempty"` - UpdatingAPullRequest bool `json:"updating-a-pull-request" yaml:"updating-a-pull-request,omitempty"` - VendorDependencies bool `json:"vendor-dependencies" yaml:"vendor-dependencies,omitempty"` - RejectExternalCode bool `json:"reject-external-code" yaml:"reject-external-code,omitempty"` - CommitMessageOptions *CommitOptions `json:"commit-message-options" yaml:"commit-message-options,omitempty"` - CredentialsMetadata []map[string]any `json:"credentials-metadata" yaml:"credentials-metadata,omitempty"` - MaxUpdaterRunTime int `json:"max-updater-run-time" yaml:"max-updater-run-time,omitempty"` + PackageManager string `json:"package-manager" yaml:"package-manager"` + AllowedUpdates []Allowed `json:"allowed-updates" yaml:"allowed-updates,omitempty"` + Dependencies []string `json:"dependencies" yaml:"dependencies,omitempty"` + ExistingPullRequests [][]ExistingPR `json:"existing-pull-requests" yaml:"existing-pull-requests,omitempty"` + Experiments Experiment `json:"experiments" yaml:"experiments,omitempty"` + IgnoreConditions []Condition `json:"ignore-conditions" yaml:"ignore-conditions,omitempty"` + LockfileOnly bool `json:"lockfile-only" yaml:"lockfile-only,omitempty"` + RequirementsUpdateStrategy *string `json:"requirements-update-strategy" yaml:"requirements-update-strategy,omitempty"` + SecurityAdvisories []Advisory `json:"security-advisories" yaml:"security-advisories,omitempty"` + SecurityUpdatesOnly bool `json:"security-updates-only" yaml:"security-updates-only,omitempty"` + Source Source `json:"source" yaml:"source"` + UpdateSubdependencies bool `json:"update-subdependencies" yaml:"update-subdependencies,omitempty"` + UpdatingAPullRequest bool `json:"updating-a-pull-request" yaml:"updating-a-pull-request,omitempty"` + VendorDependencies bool `json:"vendor-dependencies" yaml:"vendor-dependencies,omitempty"` + RejectExternalCode bool `json:"reject-external-code" yaml:"reject-external-code,omitempty"` + CommitMessageOptions *CommitOptions `json:"commit-message-options" yaml:"commit-message-options,omitempty"` + CredentialsMetadata []Credential `json:"credentials-metadata" yaml:"credentials-metadata,omitempty"` + MaxUpdaterRunTime int `json:"max-updater-run-time" yaml:"max-updater-run-time,omitempty"` } // Source is a reference to some source code @@ -86,3 +86,5 @@ type CommitOptions struct { PrefixDevelopment string `json:"prefix-development,omitempty" yaml:"prefix-development,omitempty"` IncludeScope *string `json:"include-scope,omitempty" yaml:"include-scope,omitempty"` } + +type Credential map[string]any diff --git a/internal/model/scenario.go b/internal/model/scenario.go index 8246ade..47cf1c3 100644 --- a/internal/model/scenario.go +++ b/internal/model/scenario.go @@ -13,7 +13,7 @@ type Input struct { // Job is the data given to the updater Job Job `yaml:"job"` // Credentials is the registry info and tokens to pass to the Proxy - Credentials []map[string]string `yaml:"credentials,omitempty"` + Credentials []Credential `yaml:"credentials,omitempty"` } // Output is the expected output given the inputs