Skip to content
This repository has been archived by the owner on Apr 16, 2023. It is now read-only.

Commit

Permalink
Merge pull request #30 from GoogleCloudPlatform/release
Browse files Browse the repository at this point in the history
container-builder-local now updates metadata with the correct Service Account
  • Loading branch information
bendory authored Aug 29, 2017
2 parents bfc5972 + 46c8485 commit cffd332
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 103 deletions.
4 changes: 2 additions & 2 deletions build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
40 changes: 36 additions & 4 deletions common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
}
57 changes: 50 additions & 7 deletions gcloud/gcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
154 changes: 134 additions & 20 deletions gcloud/gcloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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": "[email protected]"
}
}
}
}`
cfgNoAcct = `{
"credential": {
"access_token": "my-token"
}
}`
cfgNoExpiration = `{
"configuration": {
"properties": {
"core": {
"account": "[email protected]"
}
}
},
"credential": {
"access_token": "my-token"
}
}`
cfgExpired = `{
"configuration": {
"properties": {
"core": {
"account": "[email protected]"
}
}
},
"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 {
Expand All @@ -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
Expand All @@ -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 != "[email protected]" {
t.Errorf("AccessToken failed returning the email; got %q, want %q", token.Email, "[email protected]")
}
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")
}
}
3 changes: 3 additions & 0 deletions integration_tests/cloudbuild_auth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
steps:
- name: 'gcr.io/cloud-builders/gcloud'
args: [ "auth", "list" ]
Loading

0 comments on commit cffd332

Please sign in to comment.