diff --git a/build/build_test.go b/build/build_test.go index 0d8d269c..d4e17f46 100644 --- a/build/build_test.go +++ b/build/build_test.go @@ -154,11 +154,11 @@ func (r *mockRunner) Run(args []string, in io.Reader, out, err io.Writer, _ stri if r.remoteImages[tag] { // Successful pull. io.WriteString(out, `Using default tag: latest -latest: Pulling from test-argo/busybox +latest: Pulling from test/busybox a5d4c53980c6: Pull complete b41c5284db84: Pull complete Digest: sha256:digestRemote -Status: Downloaded newer image for gcr.io/test-argo/busybox:latest +Status: Downloaded newer image for gcr.io/test/busybox:latest `) r.localImages[tag] = true return nil diff --git a/common/common.go b/common/common.go index a81e51e1..b2addf1d 100644 --- a/common/common.go +++ b/common/common.go @@ -124,3 +124,17 @@ func Clean(r runner.Runner) error { return nil } + +// For unit tests. +var now = time.Now + +// RefreshDuration calculates when to refresh the access token. We refresh a +// bit prior to the token's expiration. +func RefreshDuration(expiration time.Time) time.Duration { + d := expiration.Sub(now()) + if d > 4*time.Second { + d = time.Duration(float64(d)*.75) + time.Second + } + + return d +} diff --git a/common/common_test.go b/common/common_test.go index 9e494d3f..d5c1ca8d 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -129,11 +129,11 @@ func TestParseSubstitutionsFlag(t *testing.T) { input: "_FOO=", want: map[string]string{"_FOO": ""}, }, { - input: "_FOO=bar,_BAR=argo", - want: map[string]string{"_FOO": "bar", "_BAR": "argo"}, + input: "_FOO=bar,_BAR=baz", + want: map[string]string{"_FOO": "bar", "_BAR": "baz"}, }, { - input: "_FOO=bar, _BAR=argo", // space between the pair - want: map[string]string{"_FOO": "bar", "_BAR": "argo"}, + input: "_FOO=bar, _BAR=baz", // space between the pair + want: map[string]string{"_FOO": "bar", "_BAR": "baz"}, }, { input: "_FOO", wantErr: true, @@ -170,3 +170,35 @@ docker volume rm id1 id2` t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got) } } + +func TestRefreshDuration(t *testing.T) { + start := time.Now() + + // control time.Now for tests. + now = func() time.Time { + return start + } + + for _, tc := range []struct { + desc string + expiration time.Time + want time.Duration + }{{ + desc: "long case", + expiration: start.Add(time.Hour), + want: 45*time.Minute + time.Second, + }, { + desc: "short case", + expiration: start.Add(4 * time.Minute), + want: 3*time.Minute + time.Second, + }, { + desc: "pathologically short", + expiration: start.Add(time.Second), + want: time.Second, + }} { + got := RefreshDuration(tc.expiration) + if got != tc.want { + t.Errorf("%s: got %q; want %q", tc.desc, got, tc.want) + } + } +} diff --git a/gcloud/gcloud.go b/gcloud/gcloud.go index b6f63114..ba8464c5 100644 --- a/gcloud/gcloud.go +++ b/gcloud/gcloud.go @@ -17,28 +17,71 @@ package gcloud import ( "bytes" + "encoding/json" + "errors" "fmt" "os" "strconv" "strings" + "time" "github.com/GoogleCloudPlatform/container-builder-local/metadata" "github.com/GoogleCloudPlatform/container-builder-local/runner" ) +var ( + errTokenNotFound = errors.New("failed to find access token") + errAcctNotFound = errors.New("failed to find credentials account") + errTokenExpired = errors.New("gcloud token is expired") +) + // AccessToken gets a fresh access token from gcloud. -func AccessToken(r runner.Runner) (string, error) { - cmd := []string{"gcloud", "config", "config-helper", "--format=value(credential.access_token)"} - var tb bytes.Buffer - if err := r.Run(cmd, nil, &tb, os.Stderr, ""); err != nil { - return "", err +func AccessToken(r runner.Runner) (*metadata.Token, error) { + // config struct matches the json output of the cmd below. + var config struct { + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + } + Configuration struct { + Properties struct { + Core struct { + Account string + } + } + } + } + + cmd := []string{"gcloud", "config", "config-helper", "--format=json"} + var b bytes.Buffer + if err := r.Run(cmd, nil, &b, os.Stderr, ""); err != nil { + return nil, err + } + if err := json.Unmarshal(b.Bytes(), &config); err != nil { + return nil, err + } + if config.Credential.AccessToken == "" { + return nil, errTokenNotFound } - return strings.TrimSpace(tb.String()), nil + if config.Configuration.Properties.Core.Account == "" { + return nil, errAcctNotFound + } + expiration, err := time.Parse(time.RFC3339, config.Credential.TokenExpiry) + if err != nil { + return nil, err + } + if expiration.Before(time.Now()) { + return nil, errTokenExpired + } + return &metadata.Token{ + AccessToken: config.Credential.AccessToken, + Expiry: expiration, + Email: config.Configuration.Properties.Core.Account, + }, nil } // ProjectInfo gets the project id and number from local gcloud. func ProjectInfo(r runner.Runner) (metadata.ProjectInfo, error) { - cmd := []string{"gcloud", "config", "list", "--format", "value(core.project)"} var idb, numb bytes.Buffer if err := r.Run(cmd, nil, &idb, os.Stderr, ""); err != nil { diff --git a/gcloud/gcloud_test.go b/gcloud/gcloud_test.go index 46619658..ad8a11dd 100644 --- a/gcloud/gcloud_test.go +++ b/gcloud/gcloud_test.go @@ -20,22 +20,86 @@ import ( "strings" "sync" "testing" + "time" ) type mockRunner struct { mu sync.Mutex - t *testing.T testCaseName string commands []string projectID string + configHelper string } -func newMockRunner(t *testing.T, projectID string) *mockRunner { - return &mockRunner{ - t: t, - projectID: projectID, +const ( + projectID = "my-project-id" + projectNum = 1234 + // validConfig is what `gcloud config config-helper --format=json` spits out + validConfig = `{ + "configuration": { + "active_configuration": "default", + "properties": { + "compute": { + "region": "us-central1", + "zone": "us-central1-f" + }, + "core": { + "account": "bogus@not-a-domain.nowhere", + "disable_usage_reporting": "False", + "project": "my-project-id" + } + } + }, + "credential": { + "access_token": "my-token", + "token_expiry": "2100-08-24T23:46:12Z" + }, + "sentinels": { + "config_sentinel": "/home/someone/.config/gcloud/config_sentinel" + } +} +` + cfgNoToken = `{ + "configuration": { + "properties": { + "core": { + "account": "bogus@not-a-domain.nowhere" + } + } } +}` + cfgNoAcct = `{ + "credential": { + "access_token": "my-token" + } +}` + cfgNoExpiration = `{ + "configuration": { + "properties": { + "core": { + "account": "bogus@not-a-domain.nowhere" + } + } + }, + "credential": { + "access_token": "my-token" + } +}` + cfgExpired = `{ + "configuration": { + "properties": { + "core": { + "account": "bogus@not-a-domain.nowhere" + } + } + }, + "credential": { + "access_token": "my-token", + "token_expiry": "1999-01-01T00:00:00Z" + } } +` +) // startsWith returns true iff arr startsWith parts. func startsWith(arr []string, parts ...string) bool { @@ -57,10 +121,10 @@ func (r *mockRunner) Run(args []string, in io.Reader, out, err io.Writer, _ stri if startsWith(args, "gcloud", "config", "list") { fmt.Fprintln(out, r.projectID) - } else if startsWith(args, "gcloud", "projects", "describe") { - fmt.Fprintln(out, "1234") - } else if startsWith(args, "gcloud", "config", "config-helper", "--format=value(credential.access_token)") { - fmt.Fprintln(out, "my-token") + } else if startsWith(args, "gcloud", "projects", "describe") && r.projectID == projectID { + fmt.Fprintln(out, projectNum) + } else if startsWith(args, "gcloud", "config", "config-helper", "--format=json") { + fmt.Fprintln(out, r.configHelper) } return nil @@ -79,45 +143,95 @@ func (r *mockRunner) Clean() error { } func TestAccessToken(t *testing.T) { - r := newMockRunner(t, "") + // Happy case. + r := &mockRunner{configHelper: validConfig} token, err := AccessToken(r) if err != nil { t.Errorf("AccessToken failed: %v", err) } - if token != "my-token" { - t.Errorf("AccessToken failed returning the token; got %s, want %s", token, "my-token") + if token.AccessToken != "my-token" { + t.Errorf("AccessToken failed returning the token; got %q, want %q", token.AccessToken, "my-token") + } + if token.Email != "bogus@not-a-domain.nowhere" { + t.Errorf("AccessToken failed returning the email; got %q, want %q", token.Email, "bogus@not-a-domain.nowhere") } got := strings.Join(r.commands, "\n") - want := "gcloud config config-helper --format=value(credential.access_token)" + want := "gcloud config config-helper --format=json" if got != want { t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got) } + + // We'll look for this error below... + _, errParseTime := time.Parse(time.RFC3339, "") + + // Unhappy cases + for _, tc := range []struct { + desc string + json string + want error + }{{ + desc: "empty json", + json: "{}", + want: errTokenNotFound, + }, { + desc: "no token", + json: cfgNoToken, + want: errTokenNotFound, + }, { + desc: "no account", + json: cfgNoAcct, + want: errAcctNotFound, + }, { + desc: "no expiration", + json: cfgNoExpiration, + want: errParseTime, + }, { + desc: "expired", + json: cfgExpired, + want: errTokenExpired, + }} { + r.configHelper = tc.json + token, err = AccessToken(r) + if err.Error() != tc.want.Error() { + t.Errorf("%s: got %v; want %v", tc.desc, err, tc.want) + } + if token != nil { + t.Errorf("%s: got unexpected token %v", tc.desc, token) + } + } } func TestProjectInfo(t *testing.T) { - r := newMockRunner(t, "my-project-id") + r := &mockRunner{projectID: projectID} projectInfo, err := ProjectInfo(r) if err != nil { t.Errorf("ProjectInfo failed: %v", err) } - if projectInfo.ProjectID != "my-project-id" { - t.Errorf("ProjectInfo failed returning the projectID; got %s, want %s", projectInfo.ProjectID, "my-project-id") + if projectInfo.ProjectID != projectID { + t.Errorf("ProjectInfo failed returning the projectID; got %s, want %s", projectInfo.ProjectID, projectID) } - if projectInfo.ProjectNum != 1234 { - t.Errorf("ProjectInfo failed returning the projectNum; got %d, want %d", projectInfo.ProjectNum, 1234) + if projectInfo.ProjectNum != projectNum { + t.Errorf("ProjectInfo failed returning the projectNum; got %d, want %d", projectInfo.ProjectNum, projectNum) } + got := strings.Join(r.commands, "\n") want := strings.Join([]string{`gcloud config list --format value(core.project)`, - `gcloud projects describe my-project-id --format value(projectNumber)`}, "\n") + fmt.Sprintf(`gcloud projects describe %s --format value(projectNumber)`, projectID)}, "\n") if got != want { t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got) } } func TestProjectInfoError(t *testing.T) { - r := newMockRunner(t, "") + r := &mockRunner{} _, err := ProjectInfo(r) if err == nil { t.Errorf("ProjectInfo should fail when no projectId set in gcloud") } + + r.projectID = "some-other-project" + _, err = ProjectInfo(r) + if err == nil { + t.Errorf("ProjectInfo should fail when no projectNum available from gcloud") + } } diff --git a/integration_tests/cloudbuild_auth.yaml b/integration_tests/cloudbuild_auth.yaml new file mode 100644 index 00000000..a838534f --- /dev/null +++ b/integration_tests/cloudbuild_auth.yaml @@ -0,0 +1,3 @@ +steps: +- name: 'gcr.io/cloud-builders/gcloud' + args: [ "auth", "list" ] diff --git a/integration_tests/cloudbuild_big.yaml b/integration_tests/cloudbuild_big.yaml index e0e392ec..8a1143fd 100644 --- a/integration_tests/cloudbuild_big.yaml +++ b/integration_tests/cloudbuild_big.yaml @@ -19,16 +19,16 @@ steps: args: - 'build' - '-t' - - 'gcr.io/argo-local-builder/test_big:tag1' + - 'gcr.io/$PROJECT_ID/test_big:tag1' - '-t' - - 'gcr.io/argo-local-builder/test_big:tag2' + - 'gcr.io/$PROJECT_ID/test_big:tag2' - '-t' - - 'gcr.io/argo-local-builder/test_big:tag3' + - 'gcr.io/$PROJECT_ID/test_big:tag3' - '.' waitFor: ['-'] images: -- 'gcr.io/argo-local-builder/test_big:tag1' -- 'gcr.io/argo-local-builder/test_big:tag2' -- 'gcr.io/argo-local-builder/test_big:tag3' +- 'gcr.io/$PROJECT_ID/test_big:tag1' +- 'gcr.io/$PROJECT_ID/test_big:tag2' +- 'gcr.io/$PROJECT_ID/test_big:tag3' diff --git a/integration_tests/cloudbuild_volumes.yaml b/integration_tests/cloudbuild_volumes.yaml new file mode 100644 index 00000000..0d7031e0 --- /dev/null +++ b/integration_tests/cloudbuild_volumes.yaml @@ -0,0 +1,22 @@ +steps: +- name: 'ubuntu' + entrypoint: 'bash' + args: ['-c', 'echo some stuff > /foo/stuff'] + volumes: + - name: 'myvol' + path: '/foo' +- name: 'ubuntu' + args: ['cat', '/foo/stuff'] + volumes: + - name: 'myvol' + path: '/foo' +# Volume can be mounted at a different path. +- name: 'ubuntu' + args: ['cat', '/bar/stuff'] + volumes: + - name: 'myvol' + path: '/bar' +# Without the volume, the file doesn't exist. +- name: 'ubuntu' + entrypoint: 'bash' + args: ['-c', 'test -f /foo/stuff || echo $?'] diff --git a/integration_tests/gce_startup_script.sh b/integration_tests/gce_startup_script.sh index a1aba851..1a3cb9bb 100644 --- a/integration_tests/gce_startup_script.sh +++ b/integration_tests/gce_startup_script.sh @@ -88,7 +88,7 @@ gsutil cp /root/output.txt $gcs_logs_path/output.txt || exit ( # If the test succeeds, copy the output to success.txt. Else, to failure.txt. cd /root/test-files - export PROJECT_ID=argo-local-builder + export PROJECT_ID=$(gcloud config list --format='value(core.project)') ./test_script.sh &> /root/output.txt && \ gsutil cp /root/output.txt $gcs_logs_path/success.txt || \ gsutil cp /root/output.txt $gcs_logs_path/failure.txt diff --git a/integration_tests/test_script.sh b/integration_tests/test_script.sh index 51b62738..b96c229c 100755 --- a/integration_tests/test_script.sh +++ b/integration_tests/test_script.sh @@ -19,5 +19,17 @@ container-builder-local --config=cloudbuild_nil.yaml --dryrun=false . || exit container-builder-local --config=cloudbuild_dockerfile.yaml --dryrun=false . || exit container-builder-local --config=cloudbuild_gcr.yaml --dryrun=false --push=true . || exit container-builder-local --config=cloudbuild_big.yaml --dryrun=false --push=true . || exit +container-builder-local --config=cloudbuild_volumes.yaml --dryrun=false . || exit + +# Confirm that we set up credentials account correctly. +WANT=$(gcloud config list --format="value(core.account)") +OUT=$(container-builder-local --config=cloudbuild_auth.yaml --dryrun=false .) +if [[ ${OUT} =~ .*${WANT}.* ]] +then + echo "PASS: auth setup" +else + echo "FAIL: auth setup" + exit 1 +fi exit 0 diff --git a/localbuilder_main.go b/localbuilder_main.go index 296ab3e4..a16596ac 100644 --- a/localbuilder_main.go +++ b/localbuilder_main.go @@ -26,7 +26,6 @@ import ( "time" computeMetadata "cloud.google.com/go/compute/metadata" - "golang.org/x/oauth2" "github.com/google/uuid" "github.com/GoogleCloudPlatform/container-builder-local/build" @@ -43,14 +42,13 @@ import ( const ( volumeNamePrefix = "cloudbuild_vol_" - tokenRefreshDur = 10 * time.Minute gcbDockerVersion = "17.05-ce" metadataImageName = "gcr.io/cloud-builders/metadata" ) var ( configFile = flag.String("config", "cloudbuild.yaml", "cloud build config file path") - substitutions = flag.String("substitutions", "", `substitutions key=value pairs separated by comma; for example _FOO=bar,_BAZ=argo`) + substitutions = flag.String("substitutions", "", `substitutions key=value pairs separated by comma; for example _FOO=bar,_BAZ=baz`) dryRun = flag.Bool("dryrun", true, "If true, nothing will be run") push = flag.Bool("push", false, "If true, the images will be pushed") help = flag.Bool("help", false, "If true, print the help message") @@ -204,31 +202,39 @@ func run(source string) error { if err != nil { return fmt.Errorf("Error getting access token to set docker credentials: %v", err) } - if err := b.SetDockerAccessToken(tok); err != nil { + if err := b.SetDockerAccessToken(tok.AccessToken); err != nil { return fmt.Errorf("Error setting docker credentials: %v", err) } - // Write initial docker credentials for GCR. This writes the initial - // ~/.docker/config.json which is made available to build steps. - - go func(stopchan <-chan struct{}) { + // Write docker credentials for GCR. This writes the initial + // ~/.docker/config.json, which is made available to build steps, and keeps + // a fresh token available. Note that the user could `gcloud auth` to + // switch accounts mid-build, and we wouldn't notice that until token + // refresh; switching accounts mid-build is not supported. + go func(tok *metadata.Token, stopchan <-chan struct{}) { for { - tok, err := gcloud.AccessToken(r) - if err != nil { - log.Printf("Error getting access token to update docker credentials: %v", err) - continue - } - if err := b.UpdateDockerAccessToken(tok); err != nil { - log.Printf("Error updating docker credentials: %v", err) + refresh := time.Duration(0) + if tok != nil { + refresh = common.RefreshDuration(tok.Expiry) } + select { - case <-time.After(tokenRefreshDur): - continue + case <-time.After(refresh): + var err error + tok, err = gcloud.AccessToken(r) + if err != nil { + log.Printf("Error getting access token to update docker credentials: %v\n", err) + continue + } + + if err := b.UpdateDockerAccessToken(tok.AccessToken); err != nil { + log.Printf("Error updating docker credentials: %v", err) + } case <-stopchan: return } } - }(stopchan) + }(tok, stopchan) } b.Start() @@ -249,23 +255,18 @@ func run(source string) error { // supplyTokenToMetadata gets gcloud token and supply it to the metadata server. func supplyTokenToMetadata(metadataUpdater metadata.RealUpdater, r runner.Runner, stopchan <-chan struct{}) { for { - accessToken, err := gcloud.AccessToken(r) + tok, err := gcloud.AccessToken(r) if err != nil { log.Printf("Error getting gcloud token: %v", err) continue } - token := oauth2.Token{ - AccessToken: accessToken, - - // https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint - Expiry: time.Now().Add(2 * tokenRefreshDur), - } - if err := metadataUpdater.SetToken(token); err != nil { + if err := metadataUpdater.SetToken(tok); err != nil { log.Printf("Error updating token in metadata server: %v", err) continue } + refresh := common.RefreshDuration(tok.Expiry) select { - case <-time.After(tokenRefreshDur): + case <-time.After(refresh): continue case <-stopchan: return diff --git a/metadata/metadata.go b/metadata/metadata.go index 7dbbe638..50f7ac14 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -38,9 +38,9 @@ import ( "strings" "time" - "github.com/GoogleCloudPlatform/container-builder-local/runner" - "golang.org/x/oauth2" + + "github.com/GoogleCloudPlatform/container-builder-local/runner" ) const ( @@ -64,7 +64,7 @@ const ( // Updater encapsulates updating the spoofed metadata server. type Updater interface { - SetToken(oauth2.Token) error + SetToken(*Token) error SetProjectInfo(ProjectInfo) error } @@ -75,14 +75,24 @@ type ProjectInfo struct { ProjectNum int64 `json:"project_num"` } -// Token represents the OAuth token request containing the access token and the -// time it expires. +// Token represents an OAuth token including the access token, account email, +// expiration, and scopes. type Token struct { AccessToken string `json:"access_token"` Expiry time.Time `json:"expiry"` + Email string `json:"email"` Scopes []string } +// Oauth2 converts a Token to a standard oauth2.Token. +func (t Token) Oauth2() *oauth2.Token { + return &oauth2.Token{ + AccessToken: t.AccessToken, + Expiry: t.Expiry, + TokenType: "Bearer", + } +} + // RealUpdater actually sends POST requests to update spoofed metadata. type RealUpdater struct { Local bool @@ -96,18 +106,14 @@ func (r RealUpdater) getAddress() string { } // SetToken updates the spoofed metadata server's credentials. -func (r RealUpdater) SetToken(tok oauth2.Token) error { +func (r RealUpdater) SetToken(tok *Token) error { scopes, err := getScopes(tok.AccessToken) if err != nil { return err } - t := Token{ - AccessToken: tok.AccessToken, - Expiry: tok.Expiry, - Scopes: scopes, - } + tok.Scopes = scopes var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(t); err != nil { + if err := json.NewEncoder(&buf).Encode(tok); err != nil { return err } diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go new file mode 100644 index 00000000..4f52ee19 --- /dev/null +++ b/metadata/metadata_test.go @@ -0,0 +1,41 @@ +// Copyright 2017 Google, Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metadata + +import ( + "testing" + "time" + + "golang.org/x/oauth2" +) + +func TestOauth2(t *testing.T) { + tok := Token{ + AccessToken: "yadda-yadda-yadda", + Expiry: time.Now(), + Email: "bogus@not-a-domain.nowhere", + } + + want := oauth2.Token{ + AccessToken: tok.AccessToken, + Expiry: tok.Expiry, + TokenType: "Bearer", + } + + got := tok.Oauth2() + if want != *got { + t.Errorf("want %v; got %v", want, got) + } +} diff --git a/subst/subst_test.go b/subst/subst_test.go index ce8ba4fb..6c8a15cc 100644 --- a/subst/subst_test.go +++ b/subst/subst_test.go @@ -203,75 +203,75 @@ func TestUserSubstitutions(t *testing.T) { desc: "variable should be substituted", template: "Hello $_VAR", substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: "Hello Argo", + want: "Hello World", }, { desc: "only full variable should be substituted", template: "Hello $_VAR_FOO, $_VAR", substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: "Hello , Argo", + want: "Hello , World", }, { desc: "variable should be substituted if sticked with a char respecting [^A-Z0-9_]", template: "Hello $_VARfoo", substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: "Hello Argofoo", + want: "Hello Worldfoo", }, { desc: "curly braced variable should be substituted", template: "Hello ${_VAR}_FOO", substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: "Hello Argo_FOO", + want: "Hello World_FOO", }, { desc: "variable should be substituted", template: "Hello, 世界 FOO$_VAR", substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: "Hello, 世界 FOOArgo", + want: "Hello, 世界 FOOWorld", }, { desc: "variable should be substituted, even if sticked", template: `Hello $_VAR$_VAR`, substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: `Hello ArgoArgo`, + want: `Hello WorldWorld`, }, { desc: "variable should be substituted, even if preceded by $$", template: `$$$_VAR`, substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: `$Argo`, + want: `$World`, }, { desc: "escaped variable should not be substituted", template: `Hello $${_VAR}_FOO, $_VAR`, substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: `Hello ${_VAR}_FOO, Argo`, + want: `Hello ${_VAR}_FOO, World`, }, { desc: "escaped variable should not be substituted", template: `Hello $$$$_VAR $$$_VAR, $_VAR, $$_VAR`, substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, - want: `Hello $$_VAR $Argo, Argo, $_VAR`, + want: `Hello $$_VAR $World, World, $_VAR`, }, { desc: "escaped variable should not be substituted", template: `$$_VAR`, substitutions: map[string]string{ - "_VAR": "Argo", + "_VAR": "World", }, want: `$_VAR`, }, { desc: "unmatched keys in the template for a built-in substitution will result in an empty string", - template: `Hello $ARGO_DEFINED_VARIABLE`, + template: `Hello $BUILTIN_DEFINED_VARIABLE`, substitutions: map[string]string{}, want: "Hello ", }} diff --git a/validate/validate_test.go b/validate/validate_test.go index 563ff178..e2f3066a 100644 --- a/validate/validate_test.go +++ b/validate/validate_test.go @@ -166,7 +166,7 @@ func TestValidateBuild(t *testing.T) { build: &cb.Build{ Id: "step-absolute-dir", Steps: []*cb.BuildStep{{ - Name: "gcr.io/test-argo/dockerize", + Name: "test", Dir: "/a/b/c", }}, }, @@ -176,7 +176,7 @@ func TestValidateBuild(t *testing.T) { build: &cb.Build{ Id: "step-parent-dir", Steps: []*cb.BuildStep{{ - Name: "gcr.io/test-argo/dockerize", + Name: "test", Dir: "../b/c", }}, }, @@ -186,7 +186,7 @@ func TestValidateBuild(t *testing.T) { build: &cb.Build{ Id: "step-parent-dir2", Steps: []*cb.BuildStep{{ - Name: "gcr.io/test-argo/dockerize", + Name: "test", Dir: "a/../b/../../c", }}, }, @@ -196,7 +196,7 @@ func TestValidateBuild(t *testing.T) { build: &cb.Build{ Id: "startstep-id", Steps: []*cb.BuildStep{{ - Name: "gcr.io/test-argo/dockerize", + Name: "test", Id: StartStep, }}, },