Skip to content

Commit cffd332

Browse files
authored
Merge pull request GoogleCloudPlatform#30 from GoogleCloudPlatform/release
container-builder-local now updates metadata with the correct Service Account
2 parents bfc5972 + 46c8485 commit cffd332

15 files changed

+391
-103
lines changed

build/build_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ func (r *mockRunner) Run(args []string, in io.Reader, out, err io.Writer, _ stri
154154
if r.remoteImages[tag] {
155155
// Successful pull.
156156
io.WriteString(out, `Using default tag: latest
157-
latest: Pulling from test-argo/busybox
157+
latest: Pulling from test/busybox
158158
a5d4c53980c6: Pull complete
159159
b41c5284db84: Pull complete
160160
Digest: sha256:digestRemote
161-
Status: Downloaded newer image for gcr.io/test-argo/busybox:latest
161+
Status: Downloaded newer image for gcr.io/test/busybox:latest
162162
`)
163163
r.localImages[tag] = true
164164
return nil

common/common.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,17 @@ func Clean(r runner.Runner) error {
124124

125125
return nil
126126
}
127+
128+
// For unit tests.
129+
var now = time.Now
130+
131+
// RefreshDuration calculates when to refresh the access token. We refresh a
132+
// bit prior to the token's expiration.
133+
func RefreshDuration(expiration time.Time) time.Duration {
134+
d := expiration.Sub(now())
135+
if d > 4*time.Second {
136+
d = time.Duration(float64(d)*.75) + time.Second
137+
}
138+
139+
return d
140+
}

common/common_test.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ func TestParseSubstitutionsFlag(t *testing.T) {
129129
input: "_FOO=",
130130
want: map[string]string{"_FOO": ""},
131131
}, {
132-
input: "_FOO=bar,_BAR=argo",
133-
want: map[string]string{"_FOO": "bar", "_BAR": "argo"},
132+
input: "_FOO=bar,_BAR=baz",
133+
want: map[string]string{"_FOO": "bar", "_BAR": "baz"},
134134
}, {
135-
input: "_FOO=bar, _BAR=argo", // space between the pair
136-
want: map[string]string{"_FOO": "bar", "_BAR": "argo"},
135+
input: "_FOO=bar, _BAR=baz", // space between the pair
136+
want: map[string]string{"_FOO": "bar", "_BAR": "baz"},
137137
}, {
138138
input: "_FOO",
139139
wantErr: true,
@@ -170,3 +170,35 @@ docker volume rm id1 id2`
170170
t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got)
171171
}
172172
}
173+
174+
func TestRefreshDuration(t *testing.T) {
175+
start := time.Now()
176+
177+
// control time.Now for tests.
178+
now = func() time.Time {
179+
return start
180+
}
181+
182+
for _, tc := range []struct {
183+
desc string
184+
expiration time.Time
185+
want time.Duration
186+
}{{
187+
desc: "long case",
188+
expiration: start.Add(time.Hour),
189+
want: 45*time.Minute + time.Second,
190+
}, {
191+
desc: "short case",
192+
expiration: start.Add(4 * time.Minute),
193+
want: 3*time.Minute + time.Second,
194+
}, {
195+
desc: "pathologically short",
196+
expiration: start.Add(time.Second),
197+
want: time.Second,
198+
}} {
199+
got := RefreshDuration(tc.expiration)
200+
if got != tc.want {
201+
t.Errorf("%s: got %q; want %q", tc.desc, got, tc.want)
202+
}
203+
}
204+
}

gcloud/gcloud.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,71 @@ package gcloud
1717

1818
import (
1919
"bytes"
20+
"encoding/json"
21+
"errors"
2022
"fmt"
2123
"os"
2224
"strconv"
2325
"strings"
26+
"time"
2427

2528
"github.com/GoogleCloudPlatform/container-builder-local/metadata"
2629
"github.com/GoogleCloudPlatform/container-builder-local/runner"
2730
)
2831

32+
var (
33+
errTokenNotFound = errors.New("failed to find access token")
34+
errAcctNotFound = errors.New("failed to find credentials account")
35+
errTokenExpired = errors.New("gcloud token is expired")
36+
)
37+
2938
// AccessToken gets a fresh access token from gcloud.
30-
func AccessToken(r runner.Runner) (string, error) {
31-
cmd := []string{"gcloud", "config", "config-helper", "--format=value(credential.access_token)"}
32-
var tb bytes.Buffer
33-
if err := r.Run(cmd, nil, &tb, os.Stderr, ""); err != nil {
34-
return "", err
39+
func AccessToken(r runner.Runner) (*metadata.Token, error) {
40+
// config struct matches the json output of the cmd below.
41+
var config struct {
42+
Credential struct {
43+
AccessToken string `json:"access_token"`
44+
TokenExpiry string `json:"token_expiry"`
45+
}
46+
Configuration struct {
47+
Properties struct {
48+
Core struct {
49+
Account string
50+
}
51+
}
52+
}
53+
}
54+
55+
cmd := []string{"gcloud", "config", "config-helper", "--format=json"}
56+
var b bytes.Buffer
57+
if err := r.Run(cmd, nil, &b, os.Stderr, ""); err != nil {
58+
return nil, err
59+
}
60+
if err := json.Unmarshal(b.Bytes(), &config); err != nil {
61+
return nil, err
62+
}
63+
if config.Credential.AccessToken == "" {
64+
return nil, errTokenNotFound
3565
}
36-
return strings.TrimSpace(tb.String()), nil
66+
if config.Configuration.Properties.Core.Account == "" {
67+
return nil, errAcctNotFound
68+
}
69+
expiration, err := time.Parse(time.RFC3339, config.Credential.TokenExpiry)
70+
if err != nil {
71+
return nil, err
72+
}
73+
if expiration.Before(time.Now()) {
74+
return nil, errTokenExpired
75+
}
76+
return &metadata.Token{
77+
AccessToken: config.Credential.AccessToken,
78+
Expiry: expiration,
79+
Email: config.Configuration.Properties.Core.Account,
80+
}, nil
3781
}
3882

3983
// ProjectInfo gets the project id and number from local gcloud.
4084
func ProjectInfo(r runner.Runner) (metadata.ProjectInfo, error) {
41-
4285
cmd := []string{"gcloud", "config", "list", "--format", "value(core.project)"}
4386
var idb, numb bytes.Buffer
4487
if err := r.Run(cmd, nil, &idb, os.Stderr, ""); err != nil {

gcloud/gcloud_test.go

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,86 @@ import (
2020
"strings"
2121
"sync"
2222
"testing"
23+
"time"
2324
)
2425

2526
type mockRunner struct {
2627
mu sync.Mutex
27-
t *testing.T
2828
testCaseName string
2929
commands []string
3030
projectID string
31+
configHelper string
3132
}
3233

33-
func newMockRunner(t *testing.T, projectID string) *mockRunner {
34-
return &mockRunner{
35-
t: t,
36-
projectID: projectID,
34+
const (
35+
projectID = "my-project-id"
36+
projectNum = 1234
37+
// validConfig is what `gcloud config config-helper --format=json` spits out
38+
validConfig = `{
39+
"configuration": {
40+
"active_configuration": "default",
41+
"properties": {
42+
"compute": {
43+
"region": "us-central1",
44+
"zone": "us-central1-f"
45+
},
46+
"core": {
47+
"account": "bogus@not-a-domain.nowhere",
48+
"disable_usage_reporting": "False",
49+
"project": "my-project-id"
50+
}
51+
}
52+
},
53+
"credential": {
54+
"access_token": "my-token",
55+
"token_expiry": "2100-08-24T23:46:12Z"
56+
},
57+
"sentinels": {
58+
"config_sentinel": "/home/someone/.config/gcloud/config_sentinel"
59+
}
60+
}
61+
`
62+
cfgNoToken = `{
63+
"configuration": {
64+
"properties": {
65+
"core": {
66+
"account": "bogus@not-a-domain.nowhere"
67+
}
68+
}
3769
}
70+
}`
71+
cfgNoAcct = `{
72+
"credential": {
73+
"access_token": "my-token"
74+
}
75+
}`
76+
cfgNoExpiration = `{
77+
"configuration": {
78+
"properties": {
79+
"core": {
80+
"account": "bogus@not-a-domain.nowhere"
81+
}
82+
}
83+
},
84+
"credential": {
85+
"access_token": "my-token"
86+
}
87+
}`
88+
cfgExpired = `{
89+
"configuration": {
90+
"properties": {
91+
"core": {
92+
"account": "bogus@not-a-domain.nowhere"
93+
}
94+
}
95+
},
96+
"credential": {
97+
"access_token": "my-token",
98+
"token_expiry": "1999-01-01T00:00:00Z"
99+
}
38100
}
101+
`
102+
)
39103

40104
// startsWith returns true iff arr startsWith parts.
41105
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
57121

58122
if startsWith(args, "gcloud", "config", "list") {
59123
fmt.Fprintln(out, r.projectID)
60-
} else if startsWith(args, "gcloud", "projects", "describe") {
61-
fmt.Fprintln(out, "1234")
62-
} else if startsWith(args, "gcloud", "config", "config-helper", "--format=value(credential.access_token)") {
63-
fmt.Fprintln(out, "my-token")
124+
} else if startsWith(args, "gcloud", "projects", "describe") && r.projectID == projectID {
125+
fmt.Fprintln(out, projectNum)
126+
} else if startsWith(args, "gcloud", "config", "config-helper", "--format=json") {
127+
fmt.Fprintln(out, r.configHelper)
64128
}
65129

66130
return nil
@@ -79,45 +143,95 @@ func (r *mockRunner) Clean() error {
79143
}
80144

81145
func TestAccessToken(t *testing.T) {
82-
r := newMockRunner(t, "")
146+
// Happy case.
147+
r := &mockRunner{configHelper: validConfig}
83148
token, err := AccessToken(r)
84149
if err != nil {
85150
t.Errorf("AccessToken failed: %v", err)
86151
}
87-
if token != "my-token" {
88-
t.Errorf("AccessToken failed returning the token; got %s, want %s", token, "my-token")
152+
if token.AccessToken != "my-token" {
153+
t.Errorf("AccessToken failed returning the token; got %q, want %q", token.AccessToken, "my-token")
154+
}
155+
if token.Email != "bogus@not-a-domain.nowhere" {
156+
t.Errorf("AccessToken failed returning the email; got %q, want %q", token.Email, "bogus@not-a-domain.nowhere")
89157
}
90158
got := strings.Join(r.commands, "\n")
91-
want := "gcloud config config-helper --format=value(credential.access_token)"
159+
want := "gcloud config config-helper --format=json"
92160
if got != want {
93161
t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got)
94162
}
163+
164+
// We'll look for this error below...
165+
_, errParseTime := time.Parse(time.RFC3339, "")
166+
167+
// Unhappy cases
168+
for _, tc := range []struct {
169+
desc string
170+
json string
171+
want error
172+
}{{
173+
desc: "empty json",
174+
json: "{}",
175+
want: errTokenNotFound,
176+
}, {
177+
desc: "no token",
178+
json: cfgNoToken,
179+
want: errTokenNotFound,
180+
}, {
181+
desc: "no account",
182+
json: cfgNoAcct,
183+
want: errAcctNotFound,
184+
}, {
185+
desc: "no expiration",
186+
json: cfgNoExpiration,
187+
want: errParseTime,
188+
}, {
189+
desc: "expired",
190+
json: cfgExpired,
191+
want: errTokenExpired,
192+
}} {
193+
r.configHelper = tc.json
194+
token, err = AccessToken(r)
195+
if err.Error() != tc.want.Error() {
196+
t.Errorf("%s: got %v; want %v", tc.desc, err, tc.want)
197+
}
198+
if token != nil {
199+
t.Errorf("%s: got unexpected token %v", tc.desc, token)
200+
}
201+
}
95202
}
96203

97204
func TestProjectInfo(t *testing.T) {
98-
r := newMockRunner(t, "my-project-id")
205+
r := &mockRunner{projectID: projectID}
99206
projectInfo, err := ProjectInfo(r)
100207
if err != nil {
101208
t.Errorf("ProjectInfo failed: %v", err)
102209
}
103-
if projectInfo.ProjectID != "my-project-id" {
104-
t.Errorf("ProjectInfo failed returning the projectID; got %s, want %s", projectInfo.ProjectID, "my-project-id")
210+
if projectInfo.ProjectID != projectID {
211+
t.Errorf("ProjectInfo failed returning the projectID; got %s, want %s", projectInfo.ProjectID, projectID)
105212
}
106-
if projectInfo.ProjectNum != 1234 {
107-
t.Errorf("ProjectInfo failed returning the projectNum; got %d, want %d", projectInfo.ProjectNum, 1234)
213+
if projectInfo.ProjectNum != projectNum {
214+
t.Errorf("ProjectInfo failed returning the projectNum; got %d, want %d", projectInfo.ProjectNum, projectNum)
108215
}
216+
109217
got := strings.Join(r.commands, "\n")
110218
want := strings.Join([]string{`gcloud config list --format value(core.project)`,
111-
`gcloud projects describe my-project-id --format value(projectNumber)`}, "\n")
219+
fmt.Sprintf(`gcloud projects describe %s --format value(projectNumber)`, projectID)}, "\n")
112220
if got != want {
113221
t.Errorf("Commands didn't match!\n===Want:\n%s\n===Got:\n%s", want, got)
114222
}
115223
}
116224

117225
func TestProjectInfoError(t *testing.T) {
118-
r := newMockRunner(t, "")
226+
r := &mockRunner{}
119227
_, err := ProjectInfo(r)
120228
if err == nil {
121229
t.Errorf("ProjectInfo should fail when no projectId set in gcloud")
122230
}
231+
232+
r.projectID = "some-other-project"
233+
_, err = ProjectInfo(r)
234+
if err == nil {
235+
t.Errorf("ProjectInfo should fail when no projectNum available from gcloud")
236+
}
123237
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
steps:
2+
- name: 'gcr.io/cloud-builders/gcloud'
3+
args: [ "auth", "list" ]

0 commit comments

Comments
 (0)