Skip to content

Commit

Permalink
refactoring to make gke-gcloud-auth-plugin unit testable
Browse files Browse the repository at this point in the history
Also:
* some random cleanup to make the code a bit more go idiomatic.
* let gcloud stderr go to stderr. it's forwarded from the parent.
* don't spit out stdout from gcloud when we error because it could
  contain secrets.
  • Loading branch information
mikedanese committed Feb 9, 2022
1 parent e8b53ad commit 90cc8ca
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitallowed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ya29.t0k3n
12 changes: 12 additions & 0 deletions cmd/gke-gcloud-auth-plugin/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
)
load("//defs:version.bzl", "version_x_defs")

Expand All @@ -26,3 +27,14 @@ go_library(
"//vendor/k8s.io/component-base/version/verflag:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["main_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1:go_default_library",
],
)
126 changes: 72 additions & 54 deletions cmd/gke-gcloud-auth-plugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"time"

"github.com/spf13/pflag"
"golang.org/x/oauth2/google"

meta "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauth "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/component-base/version/verflag"
Expand All @@ -22,48 +25,70 @@ var (
// email instead of numeric uniqueID.
defaultScopes = []string{
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"}
"https://www.googleapis.com/auth/userinfo.email",
}
)

type credential struct {
AccessToken string `json:"access_token"`
TokenExpiry time.Time `json:"token_expiry"`
X map[string]interface{} `json:"-"` // Rest of the fields should go here.
}
// types unmarshaled from gcloud config in json format
type (
gcloudConfiguration struct {
Credential credential `json:"credential"`
}

// gcloudConfiguration is the struct unmarshalled
// from gcloud config in json format
type gcloudConfiguration struct {
Credential credential `json:"credential"`
X map[string]interface{} `json:"-"` // Rest of the fields should go here.
}
credential struct {
AccessToken string `json:"access_token"`
TokenExpiry time.Time `json:"token_expiry"`
}
)

var (
useAdcPtr = pflag.Bool("use_application_default_credentials", false, "returns exec credential filled with application default credentials.")
useApplicationDefaultCredentials = pflag.Bool("use_application_default_credentials", false, "returns exec credential filled with application default credentials.")
)

func main() {
pflag.Parse()
verflag.PrintAndExitIfRequested()

ec, err := execCredential()
p := plugin{
useApplicationDefaultCredentials: *useApplicationDefaultCredentials,
readGcloudConfigRaw: readGcloudConfigRaw,
w: os.Stdout,
}

if err := p.run(); err != nil {
fmt.Fprintf(os.Stderr, "Unable to retrieve access token for GKE: %s", err)
os.Exit(1)
}
}

type plugin struct {
useApplicationDefaultCredentials bool
readGcloudConfigRaw func() ([]byte, error)
w io.Writer
}

func (p *plugin) run() error {
creds, err := p.execCredential()
if err != nil {
msg := fmt.Errorf("unable to retrieve access token for GKE. Error : %v", err)
panic(msg)
return fmt.Errorf("unable to retrieve access token for GKE: %w", err)
}

ecStr, err := formatToJSON(ec)
out, err := json.Marshal(creds)
if err != nil {
msg := fmt.Errorf("unable to convert ExecCredential object to json format. Error :%v", err)
panic(msg)
return fmt.Errorf("unable to convert ExecCredential object to json format: %w", err)
}

if _, err := p.w.Write(out); err != nil {
return fmt.Errorf("unable to write ExecCredential to stdout: %w", err)
}
fmt.Print(ecStr)

return nil
}

// ExecCredential return an object of type ExecCredential which
// execCredential return an object of type ExecCredential which
// holds a bearer token to authenticate to GKE.
func execCredential() (*clientauth.ExecCredential, error) {
token, expiry, err := accessToken()
func (p *plugin) execCredential() (*clientauth.ExecCredential, error) {
token, expiry, err := p.accessToken()
if err != nil {
return nil, err
}
Expand All @@ -80,70 +105,63 @@ func execCredential() (*clientauth.ExecCredential, error) {
}, nil
}

func accessToken() (string, *meta.Time, error) {
if !*useAdcPtr {
token, expiry, err := gcloudAccessToken()
func (p *plugin) accessToken() (string, *meta.Time, error) {
if !p.useApplicationDefaultCredentials {
token, expiry, err := p.gcloudAccessToken()
if err == nil {
return token, expiry, nil
}
}
return defaultAccessToken()
return p.defaultAccessToken()
}

func gcloudAccessToken() (string, *meta.Time, error) {
gc, err := retrieveGcloudConfig()
func (p *plugin) gcloudAccessToken() (string, *meta.Time, error) {
gc, err := p.readGcloudConfig()
if err != nil {
return "", nil, err
}

return gc.Credential.AccessToken, &meta.Time{Time: gc.Credential.TokenExpiry}, nil
}

func defaultAccessToken() (string, *meta.Time, error) {
func (p *plugin) defaultAccessToken() (string, *meta.Time, error) {
ts, err := google.DefaultTokenSource(context.Background(), defaultScopes...)
if err != nil {
return "", nil, fmt.Errorf("cannot construct google default token source: %v", err)
return "", nil, fmt.Errorf("cannot construct google default token source: %w", err)
}

tok, err := ts.Token()
if err != nil {
return "", nil, fmt.Errorf("cannot retrieve default token from google default token source: %v", err)
return "", nil, fmt.Errorf("cannot retrieve default token from google default token source: %w", err)
}

return tok.AccessToken, &meta.Time{Time: tok.Expiry}, nil
}

// retrieveGcloudConfig returns an object which represents gcloud config output
func retrieveGcloudConfig() (*gcloudConfiguration, error) {
gcloudConfigbytes, err := gcloudConfigOutput()
// readGcloudConfig returns an object which represents gcloud config output
func (p *plugin) readGcloudConfig() (*gcloudConfiguration, error) {
gcRaw, err := p.readGcloudConfigRaw()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to retrieve gcloud config: %w", err)
}

var gc gcloudConfiguration
if err := json.Unmarshal(gcloudConfigbytes, &gc); err != nil {
return nil, fmt.Errorf("error parsing gcloud output : %+v", err.Error())
if err := json.Unmarshal(gcRaw, &gc); err != nil {
return nil, fmt.Errorf("error parsing gcloud output : %w", err)
}

return &gc, nil
}

func gcloudConfigOutput() ([]byte, error) {
func readGcloudConfigRaw() ([]byte, error) {
cmd := exec.Command("gcloud", "config", "config-helper", "--format=json")
var stdoutBuffer bytes.Buffer
var stderrBuffer bytes.Buffer
cmd.Stdout = &stdoutBuffer
cmd.Stderr = &stderrBuffer
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("failed to retrieve gcloud config. Error message: %s, stdout: %s, stderr: %s", err.Error(), stdoutBuffer.String(), stderrBuffer.String())
}
return stdoutBuffer.Bytes(), nil
}

func formatToJSON(i interface{}) (string, error) {
s, err := json.MarshalIndent(i, "", " ")
if err != nil {
return "", err
var buf bytes.Buffer
cmd.Stdout = &buf

if err := cmd.Run(); err != nil {
return nil, err
}
return string(s), nil

return buf.Bytes(), nil
}
90 changes: 90 additions & 0 deletions cmd/gke-gcloud-auth-plugin/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauth "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)

func TestGcloudPlugin(t *testing.T) {
newYears := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

tcs := []struct {
name string
config string

wantErr bool
wantStatus *clientauth.ExecCredentialStatus
}{
{
name: "good",
config: `
{
"credential": {
"access_token": "ya29.t0k3n",
"token_expiry": "2022-01-01T00:00:00Z"
}
}
`,
wantStatus: &clientauth.ExecCredentialStatus{
Token: "ya29.t0k3n",
ExpirationTimestamp: &metav1.Time{Time: newYears},
},
},
{
name: "all good with details",
config: `
{
"configuration": {
"active_configuration": "default",
"properties": {
"compute": {
"region": "hoth-echobase1",
"zone": "hoth-echobase1-c"
},
"core": {
"account": "[email protected]",
"disable_usage_reporting": "True",
"project": "the-resistance"
}
}
},
"credential": {
"access_token": "ya29.t0k3n",
"token_expiry": "2022-01-01T00:00:00Z"
}
}
`,
wantStatus: &clientauth.ExecCredentialStatus{
Token: "ya29.t0k3n",
ExpirationTimestamp: &metav1.Time{Time: newYears},
},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
p := &plugin{
readGcloudConfigRaw: func() ([]byte, error) {
return []byte(tc.config), nil
},
}

creds, err := p.execCredential()
if (err != nil) != tc.wantErr {
t.Fatalf("wantErr=%v, err=%v", tc.wantErr, err)
}
if tc.wantErr {
t.Log(err)
return
}

if diff := cmp.Diff(tc.wantStatus, creds.Status); diff != "" {
t.Errorf("execCredential() returned unexpected diff (-want +got): %s", diff)
}
})
}
}

0 comments on commit 90cc8ca

Please sign in to comment.