From 05c94414e90b3fe539a4bd1bf438642badd188e9 Mon Sep 17 00:00:00 2001 From: Omar Jarjur <ojarjur@google.com> Date: Mon, 2 Jul 2018 14:40:15 -0700 Subject: [PATCH 1/4] make SDKConfig work with newer versions of gcloud The existing implementation of SDKConfig assumes that the Cloud SDK writes a config file named `credentials`, which contains JSON-encoded OAuth2 credentials for the user. Since version 161.0.0 of the Cloud SDK, that has not been true, as the Cloud SDK has switched to storing credentials in a SQLite database instead of a JSON-encoded file. This change switches from trying to directly read the underlying credential store (which it is now clear can change without notice), to instead invoking the `gcloud` command and asking it for its credentials. This is done using the subcommand ```sh gcloud config config-helper ``` That subcommand is meant to provide auth data to external tools, so it seems to be a more appropriate choice than reading the underlying storage directly. This fixes #300 --- google/sdk.go | 171 +++++++++++++-------------------------------- google/sdk_test.go | 129 +++++++++++----------------------- 2 files changed, 90 insertions(+), 210 deletions(-) diff --git a/google/sdk.go b/google/sdk.go index b9660cadd..76c41ea19 100644 --- a/google/sdk.go +++ b/google/sdk.go @@ -5,15 +5,13 @@ package google import ( - "bufio" + "bytes" "encoding/json" - "errors" "fmt" - "io" "net/http" "os" + "os/exec" "os/user" - "path/filepath" "runtime" "strings" "time" @@ -22,27 +20,19 @@ import ( "golang.org/x/oauth2" ) -type sdkCredentials struct { - Data []struct { - Credential struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenExpiry *time.Time `json:"token_expiry"` - } `json:"credential"` - Key struct { - Account string `json:"account"` - Scope string `json:"scope"` - } `json:"key"` - } +type configHelperResp struct { + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + } `json:"credential"` } +type configHelper func() (*configHelperResp, error) + // An SDKConfig provides access to tokens from an account already // authorized via the Google Cloud SDK. type SDKConfig struct { - conf oauth2.Config - initialToken *oauth2.Token + helper configHelper } // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK @@ -52,72 +42,37 @@ type SDKConfig struct { // before using this function. // The Google Cloud SDK is available at https://cloud.google.com/sdk/. func NewSDKConfig(account string) (*SDKConfig, error) { - configPath, err := sdkConfigPath() - if err != nil { - return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) - } - credentialsPath := filepath.Join(configPath, "credentials") - f, err := os.Open(credentialsPath) - if err != nil { - return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) - } - defer f.Close() - - var c sdkCredentials - if err := json.NewDecoder(f).Decode(&c); err != nil { - return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) - } - if len(c.Data) == 0 { - return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) - } + gcloudCmd := gcloudCommand() if account == "" { - propertiesPath := filepath.Join(configPath, "properties") - f, err := os.Open(propertiesPath) - if err != nil { - return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) + cmd := exec.Command(gcloudCmd, "auth", "list", "--filter=status=ACTIVE", "--format=value(account)") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failure looking up the active Cloud SDK account: %v", err) } - defer f.Close() - ini, err := parseINI(f) - if err != nil { - return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) - } - core, ok := ini["core"] - if !ok { - return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) + account = strings.TrimSpace(out.String()) + } + helper := func() (*configHelperResp, error) { + cmd := exec.Command(gcloudCmd, "config", "config-helper", "--account", account, "--format=json") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, err } - active, ok := core["account"] - if !ok { - return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) + var resp configHelperResp + if err := json.Unmarshal(out.Bytes(), &resp); err != nil { + return nil, fmt.Errorf("failure parsing the output from the Cloud SDK config helper: %v", err) } - account = active + return &resp, nil } + return &SDKConfig{helper}, nil +} - for _, d := range c.Data { - if account == "" || d.Key.Account == account { - if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { - return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) - } - var expiry time.Time - if d.Credential.TokenExpiry != nil { - expiry = *d.Credential.TokenExpiry - } - return &SDKConfig{ - conf: oauth2.Config{ - ClientID: d.Credential.ClientID, - ClientSecret: d.Credential.ClientSecret, - Scopes: strings.Split(d.Key.Scope, " "), - Endpoint: Endpoint, - RedirectURL: "oob", - }, - initialToken: &oauth2.Token{ - AccessToken: d.Credential.AccessToken, - RefreshToken: d.Credential.RefreshToken, - Expiry: expiry, - }, - }, nil - } +func gcloudCommand() string { + if runtime.GOOS == "windows" { + return "gcloud.cmd" } - return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) + return "gcloud" } // Client returns an HTTP client using Google Cloud SDK credentials to @@ -128,7 +83,7 @@ func NewSDKConfig(account string) (*SDKConfig, error) { func (c *SDKConfig) Client(ctx context.Context) *http.Client { return &http.Client{ Transport: &oauth2.Transport{ - Source: c.TokenSource(ctx), + Source: c, }, } } @@ -139,53 +94,23 @@ func (c *SDKConfig) Client(ctx context.Context) *http.Client { // and refresh it when it expires, but it won't update the credentials // with the new access token. func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { - return c.conf.TokenSource(ctx, c.initialToken) + return c } -// Scopes are the OAuth 2.0 scopes the current account is authorized for. -func (c *SDKConfig) Scopes() []string { - return c.conf.Scopes -} - -func parseINI(ini io.Reader) (map[string]map[string]string, error) { - result := map[string]map[string]string{ - "": {}, // root section - } - scanner := bufio.NewScanner(ini) - currentSection := "" - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, ";") { - // comment. - continue - } - if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { - currentSection = strings.TrimSpace(line[1 : len(line)-1]) - result[currentSection] = map[string]string{} - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 && parts[0] != "" { - result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error scanning ini: %v", err) - } - return result, nil -} - -// sdkConfigPath tries to guess where the gcloud config is located. -// It can be overridden during tests. -var sdkConfigPath = func() (string, error) { - if runtime.GOOS == "windows" { - return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil +// Token returns an oauth2.Token retrieved from the Google Cloud SDK. +func (c *SDKConfig) Token() (*oauth2.Token, error) { + resp, err := c.helper() + if err != nil { + return nil, fmt.Errorf("failure invoking the Cloud SDK config helper: %v", err) } - homeDir := guessUnixHomeDir() - if homeDir == "" { - return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") + expiry, err := time.Parse(time.RFC3339, resp.Credential.TokenExpiry) + if err != nil { + return nil, fmt.Errorf("failure parsing the access token expiration time: %v", err) } - return filepath.Join(homeDir, ".config", "gcloud"), nil + return &oauth2.Token{ + AccessToken: resp.Credential.AccessToken, + Expiry: expiry, + }, nil } func guessUnixHomeDir() string { diff --git a/google/sdk_test.go b/google/sdk_test.go index 52b8ecada..6f718f387 100644 --- a/google/sdk_test.go +++ b/google/sdk_test.go @@ -5,103 +5,58 @@ package google import ( - "reflect" - "strings" + "fmt" "testing" + "time" ) func TestSDKConfig(t *testing.T) { - sdkConfigPath = func() (string, error) { - return "testdata/gcloud", nil - } - - tests := []struct { - account string - accessToken string - err bool - }{ - {"", "bar_access_token", false}, - {"foo@example.com", "foo_access_token", false}, - {"bar@example.com", "bar_access_token", false}, - {"baz@serviceaccount.example.com", "", true}, + var helperCallCount int + mockTokenFormat := "Token #%d" + mockHelper := func() (*configHelperResp, error) { + token := fmt.Sprintf(mockTokenFormat, helperCallCount) + helperCallCount += 1 + return &configHelperResp{ + Credential: struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + }{ + AccessToken: token, + TokenExpiry: time.Now().Format(time.RFC3339), + }, + }, nil } - for _, tt := range tests { - c, err := NewSDKConfig(tt.account) - if got, want := err != nil, tt.err; got != want { - if !tt.err { - t.Errorf("got %v, want nil", err) - } else { - t.Errorf("got nil, want error") - } - continue - } + mockConfig := &SDKConfig{mockHelper} + for i := 0; i < 10; i++ { + tok, err := mockConfig.Token() if err != nil { - continue - } - tok := c.initialToken - if tok == nil { - t.Errorf("got nil, want %q", tt.accessToken) - continue - } - if tok.AccessToken != tt.accessToken { - t.Errorf("got %q, want %q", tok.AccessToken, tt.accessToken) + t.Errorf("Unexpected error reading a mock config helper response: %v", err) + } else if got, want := tok.AccessToken, fmt.Sprintf(mockTokenFormat, i); got != want { + t.Errorf("Got access token of %q; wanted %q", got, want) } } -} -func TestParseINI(t *testing.T) { - tests := []struct { - ini string - want map[string]map[string]string - }{ - { - `root = toor -[foo] -bar = hop -ini = nin -`, - map[string]map[string]string{ - "": {"root": "toor"}, - "foo": {"bar": "hop", "ini": "nin"}, - }, - }, - { - "\t extra \t = whitespace \t\r\n \t [everywhere] \t \r\n here \t = \t there \t \r\n", - map[string]map[string]string{ - "": {"extra": "whitespace"}, - "everywhere": {"here": "there"}, - }, - }, - { - `[empty] -[section] -empty= -`, - map[string]map[string]string{ - "": {}, - "empty": {}, - "section": {"empty": ""}, - }, - }, - { - `ignore -[invalid -=stuff -;comment=true -`, - map[string]map[string]string{ - "": {}, + failingHelper := func() (*configHelperResp, error) { + return nil, fmt.Errorf("mock config helper failure") + } + failingConfig := &SDKConfig{failingHelper} + if tok, err := failingConfig.Token(); err == nil { + t.Errorf("unexpected token response for failing helper: got %v", tok) + } + + badTimestampHelper := func() (*configHelperResp, error) { + return &configHelperResp{ + Credential: struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + }{ + AccessToken: "Fake token", + TokenExpiry: "The time at which it expires", }, - }, + }, nil } - for _, tt := range tests { - result, err := parseINI(strings.NewReader(tt.ini)) - if err != nil { - t.Errorf("parseINI(%q) error %v, want: no error", tt.ini, err) - continue - } - if !reflect.DeepEqual(result, tt.want) { - t.Errorf("parseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want) - } + badTimestampConfig := &SDKConfig{badTimestampHelper} + if tok, err := badTimestampConfig.Token(); err == nil { + t.Errorf("unexpected token response for a helper that returns bad expiry timestamps: got %v", tok) } } From ebcc2cd2a097889599e7533698cd1c36e2aaea15 Mon Sep 17 00:00:00 2001 From: Omar Jarjur <ojarjur@google.com> Date: Thu, 20 Dec 2018 13:39:53 -0800 Subject: [PATCH 2/4] google: make the sdk error messages consistent This change makes the error messages generated by the sdk component consistent both in terms of contents and in terms of when returned errors are wrapped. --- google/sdk.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/google/sdk.go b/google/sdk.go index f9cb7f94f..bf584ce3a 100644 --- a/google/sdk.go +++ b/google/sdk.go @@ -48,7 +48,7 @@ func NewSDKConfig(account string) (*SDKConfig, error) { var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("failure looking up the active Cloud SDK account: %v", err) + return nil, fmt.Errorf("looking up the active Cloud SDK account: %v", err) } account = strings.TrimSpace(out.String()) } @@ -57,11 +57,11 @@ func NewSDKConfig(account string) (*SDKConfig, error) { var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { - return nil, err + return nil, fmt.Errorf("running the config-helper command: %v", err) } var resp configHelperResp if err := json.Unmarshal(out.Bytes(), &resp); err != nil { - return nil, fmt.Errorf("failure parsing the output from the Cloud SDK config helper: %v", err) + return nil, fmt.Errorf("parsing the config-helper output: %v", err) } return &resp, nil } @@ -101,11 +101,12 @@ func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { func (c *SDKConfig) Token() (*oauth2.Token, error) { resp, err := c.helper() if err != nil { - return nil, fmt.Errorf("failure invoking the Cloud SDK config helper: %v", err) + return nil, err } - expiry, err := time.Parse(time.RFC3339, resp.Credential.TokenExpiry) + expiryStr := resp.Credential.TokenExpiry + expiry, err := time.Parse(time.RFC3339, expiryStr) if err != nil { - return nil, fmt.Errorf("failure parsing the access token expiration time: %v", err) + return nil, fmt.Errorf("parsing the access token expiry time %q: %v", expiryStr, err) } return &oauth2.Token{ AccessToken: resp.Credential.AccessToken, From 1ebe5b940227e861223f722e975a8ef50eaffb3c Mon Sep 17 00:00:00 2001 From: Omar Jarjur <ojarjur@google.com> Date: Fri, 11 Jan 2019 14:22:45 -0800 Subject: [PATCH 3/4] Inline the logic for fetching a token from gcloud directly into the SDKConfig.Token() method --- google/sdk.go | 45 +++++++++++++++++++---------------------- google/sdk_test.go | 50 ++++++++++++++++------------------------------ 2 files changed, 38 insertions(+), 57 deletions(-) diff --git a/google/sdk.go b/google/sdk.go index bf584ce3a..23a2a6eca 100644 --- a/google/sdk.go +++ b/google/sdk.go @@ -20,6 +20,7 @@ import ( "golang.org/x/oauth2" ) +// configHelperResp corresponds to the JSON output of the `gcloud config-helper` command. type configHelperResp struct { Credential struct { AccessToken string `json:"access_token"` @@ -27,12 +28,10 @@ type configHelperResp struct { } `json:"credential"` } -type configHelper func() (*configHelperResp, error) - // An SDKConfig provides access to tokens from an account already // authorized via the Google Cloud SDK. type SDKConfig struct { - helper configHelper + account string } // NewSDKConfig creates an SDKConfig for the given Google Cloud SDK @@ -52,20 +51,7 @@ func NewSDKConfig(account string) (*SDKConfig, error) { } account = strings.TrimSpace(out.String()) } - helper := func() (*configHelperResp, error) { - cmd := exec.Command(gcloudCmd, "config", "config-helper", "--account", account, "--format=json") - var out bytes.Buffer - cmd.Stdout = &out - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("running the config-helper command: %v", err) - } - var resp configHelperResp - if err := json.Unmarshal(out.Bytes(), &resp); err != nil { - return nil, fmt.Errorf("parsing the config-helper output: %v", err) - } - return &resp, nil - } - return &SDKConfig{helper}, nil + return &SDKConfig{account}, nil } func gcloudCommand() string { @@ -97,23 +83,34 @@ func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { return c } -// Token returns an oauth2.Token retrieved from the Google Cloud SDK. -func (c *SDKConfig) Token() (*oauth2.Token, error) { - resp, err := c.helper() - if err != nil { - return nil, err +func parseConfigHelperResp(b []byte) (*oauth2.Token, error) { + var r configHelperResp + if err := json.Unmarshal(b, &r); err != nil { + return nil, fmt.Errorf("parsing the config-helper output: %v", err) } - expiryStr := resp.Credential.TokenExpiry + expiryStr := r.Credential.TokenExpiry expiry, err := time.Parse(time.RFC3339, expiryStr) if err != nil { return nil, fmt.Errorf("parsing the access token expiry time %q: %v", expiryStr, err) } return &oauth2.Token{ - AccessToken: resp.Credential.AccessToken, + AccessToken: r.Credential.AccessToken, Expiry: expiry, }, nil } +// Token returns an oauth2.Token retrieved from the Google Cloud SDK. +func (c *SDKConfig) Token() (*oauth2.Token, error) { + gcloudCmd := gcloudCommand() + cmd := exec.Command(gcloudCmd, "config", "config-helper", "--account", c.account, "--format=json") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("running the config-helper command: %v", err) + } + return parseConfigHelperResp(out.Bytes()) +} + func guessUnixHomeDir() string { // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 if v := os.Getenv("HOME"); v != "" { diff --git a/google/sdk_test.go b/google/sdk_test.go index 6f718f387..b45500ccb 100644 --- a/google/sdk_test.go +++ b/google/sdk_test.go @@ -10,53 +10,37 @@ import ( "time" ) +const mockResponseTemplate = `{ + "credential": { + "access_token": %q, + "token_expiry": %q + } +}` + func TestSDKConfig(t *testing.T) { var helperCallCount int mockTokenFormat := "Token #%d" - mockHelper := func() (*configHelperResp, error) { + mockHelper := func() []byte { token := fmt.Sprintf(mockTokenFormat, helperCallCount) helperCallCount += 1 - return &configHelperResp{ - Credential: struct { - AccessToken string `json:"access_token"` - TokenExpiry string `json:"token_expiry"` - }{ - AccessToken: token, - TokenExpiry: time.Now().Format(time.RFC3339), - }, - }, nil + return []byte(fmt.Sprintf(mockResponseTemplate, token, time.Now().Format(time.RFC3339))) } - mockConfig := &SDKConfig{mockHelper} for i := 0; i < 10; i++ { - tok, err := mockConfig.Token() + tok, err := parseConfigHelperResp(mockHelper()) if err != nil { - t.Errorf("Unexpected error reading a mock config helper response: %v", err) + t.Errorf("Unexpected error parsing a mock config helper response: %v", err) } else if got, want := tok.AccessToken, fmt.Sprintf(mockTokenFormat, i); got != want { t.Errorf("Got access token of %q; wanted %q", got, want) } } - failingHelper := func() (*configHelperResp, error) { - return nil, fmt.Errorf("mock config helper failure") - } - failingConfig := &SDKConfig{failingHelper} - if tok, err := failingConfig.Token(); err == nil { - t.Errorf("unexpected token response for failing helper: got %v", tok) + badJSON := []byte(`Not really a JSON response`) + if tok, err := parseConfigHelperResp(badJSON); err == nil { + t.Errorf("unexpected parsing result for an malformed helper response: got %v", tok) } - badTimestampHelper := func() (*configHelperResp, error) { - return &configHelperResp{ - Credential: struct { - AccessToken string `json:"access_token"` - TokenExpiry string `json:"token_expiry"` - }{ - AccessToken: "Fake token", - TokenExpiry: "The time at which it expires", - }, - }, nil - } - badTimestampConfig := &SDKConfig{badTimestampHelper} - if tok, err := badTimestampConfig.Token(); err == nil { - t.Errorf("unexpected token response for a helper that returns bad expiry timestamps: got %v", tok) + badTimestamp := []byte(fmt.Sprintf(mockResponseTemplate, "Fake Token", "The time at which it expires")) + if tok, err := parseConfigHelperResp(badTimestamp); err == nil { + t.Errorf("unexpected parsing result for a helper response with a bad expiry timestamp: got %v", tok) } } From a160a168fc116a8421f8fb4f00279908fe759e46 Mon Sep 17 00:00:00 2001 From: Omar Jarjur <ojarjur@google.com> Date: Fri, 11 Jan 2019 17:26:24 -0800 Subject: [PATCH 4/4] Simplify and shorten the code for the SDKConfig TokenSource --- google/sdk.go | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/google/sdk.go b/google/sdk.go index 23a2a6eca..0af04c601 100644 --- a/google/sdk.go +++ b/google/sdk.go @@ -5,7 +5,6 @@ package google import ( - "bytes" "context" "encoding/json" "fmt" @@ -13,7 +12,6 @@ import ( "os" "os/exec" "os/user" - "runtime" "strings" "time" @@ -31,6 +29,12 @@ type configHelperResp struct { // An SDKConfig provides access to tokens from an account already // authorized via the Google Cloud SDK. type SDKConfig struct { + // account is the name of the gcloud-authenticated account whose credentials should be used + // to generate OAuth tokens. This should be one of the accounts listed in the output of + // `gcloud auth list`. + // + // For instance, if the user logged in to gcloud with the account `user@example.com`, then + // they could use `user@example.com` as the value for this field. account string } @@ -41,24 +45,16 @@ type SDKConfig struct { // before using this function. // The Google Cloud SDK is available at https://cloud.google.com/sdk/. func NewSDKConfig(account string) (*SDKConfig, error) { - gcloudCmd := gcloudCommand() - if account == "" { - cmd := exec.Command(gcloudCmd, "auth", "list", "--filter=status=ACTIVE", "--format=value(account)") - var out bytes.Buffer - cmd.Stdout = &out - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("looking up the active Cloud SDK account: %v", err) - } - account = strings.TrimSpace(out.String()) + if account != "" { + return &SDKConfig{account}, nil } - return &SDKConfig{account}, nil -} - -func gcloudCommand() string { - if runtime.GOOS == "windows" { - return "gcloud.cmd" + cmd := exec.Command("gcloud", "auth", "list", "--filter=status=ACTIVE", "--format=value(account)") + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("looking up the active Cloud SDK account: %v", err) } - return "gcloud" + account = strings.TrimSpace(string(out)) + return &SDKConfig{account}, nil } // Client returns an HTTP client using Google Cloud SDK credentials to @@ -91,7 +87,7 @@ func parseConfigHelperResp(b []byte) (*oauth2.Token, error) { expiryStr := r.Credential.TokenExpiry expiry, err := time.Parse(time.RFC3339, expiryStr) if err != nil { - return nil, fmt.Errorf("parsing the access token expiry time %q: %v", expiryStr, err) + return nil, fmt.Errorf("parsing the access token expiry time: %v", err) } return &oauth2.Token{ AccessToken: r.Credential.AccessToken, @@ -101,14 +97,12 @@ func parseConfigHelperResp(b []byte) (*oauth2.Token, error) { // Token returns an oauth2.Token retrieved from the Google Cloud SDK. func (c *SDKConfig) Token() (*oauth2.Token, error) { - gcloudCmd := gcloudCommand() - cmd := exec.Command(gcloudCmd, "config", "config-helper", "--account", c.account, "--format=json") - var out bytes.Buffer - cmd.Stdout = &out - if err := cmd.Run(); err != nil { + cmd := exec.Command("gcloud", "config", "config-helper", "--account", c.account, "--format=json") + out, err := cmd.Output() + if err != nil { return nil, fmt.Errorf("running the config-helper command: %v", err) } - return parseConfigHelperResp(out.Bytes()) + return parseConfigHelperResp(out) } func guessUnixHomeDir() string {