Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions providers/gce/gce.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package gce

import (
"context"
"encoding/json"
"fmt"
"io"
"runtime"
Expand Down Expand Up @@ -558,9 +559,46 @@ func GenerateCloudConfig(configFile *ConfigFile) (cloudConfig *CloudConfig, err
return cloudConfig, err
}

// clientOptions returns GCP API client options for authentication.
// A custom TokenSource set in the config, is used directly.
// Otherwise, FindDefaultCredentials discovers credentials.
// WithCredentialsJSON is preferred when available as it uses
// self-signed JWTs, which may be necessary for custom universe domains.
func clientOptions(ts oauth2.TokenSource) ([]option.ClientOption, error) {
if ts != nil {
return []option.ClientOption{option.WithTokenSource(ts)}, nil
}

creds, err := google.FindDefaultCredentials(context.Background(), compute.CloudPlatformScope)
if err != nil {
return nil, fmt.Errorf("failed to find default credentials: %w", err)
}

var opts []option.ClientOption
if len(creds.JSON) > 0 {
var f struct {
Type string `json:"type"`
}
if err := json.Unmarshal(creds.JSON, &f); err != nil {
return nil, fmt.Errorf("failed to parse credentials JSON: %w", err)
}
opts = []option.ClientOption{option.WithAuthCredentialsJSON(option.CredentialsType(f.Type), creds.JSON)}
} else {
opts = []option.ClientOption{option.WithCredentials(creds)}
}

if ud, err := creds.GetUniverseDomain(); err == nil {
opts = append(opts, option.WithUniverseDomain(ud))
} else {
klog.Warningf("Failed to get universe domain from credentials: %v", err)
}

return opts, nil
}

// CreateGCECloud creates a Cloud object using the specified parameters.
// If no networkUrl is specified, loads networkName via rest call.
// If no tokenSource is specified, uses oauth2.DefaultTokenSource.
// If no tokenSource is specified, uses FindDefaultCredentials.
// If managedZones is nil / empty all zones in the region will be managed.
func CreateGCECloud(config *CloudConfig) (*Cloud, error) {
// If ManagedZones was empty at startup, it means the cluster was configured
Expand All @@ -580,19 +618,24 @@ func CreateGCECloud(config *CloudConfig) (*Cloud, error) {
config.NetworkProjectID = config.ProjectID
}

service, err := compute.NewService(context.Background(), option.WithTokenSource(config.TokenSource))
clientOpts, err := clientOptions(config.TokenSource)
if err != nil {
return nil, err
}

service, err := compute.NewService(context.Background(), clientOpts...)
if err != nil {
return nil, err
}
service.UserAgent = userAgent

serviceBeta, err := computebeta.NewService(context.Background(), option.WithTokenSource(config.TokenSource))
serviceBeta, err := computebeta.NewService(context.Background(), clientOpts...)
if err != nil {
return nil, err
}
serviceBeta.UserAgent = userAgent

serviceAlpha, err := computealpha.NewService(context.Background(), option.WithTokenSource(config.TokenSource))
serviceAlpha, err := computealpha.NewService(context.Background(), clientOpts...)
if err != nil {
return nil, err
}
Expand All @@ -610,7 +653,7 @@ func CreateGCECloud(config *CloudConfig) (*Cloud, error) {
}
}

containerService, err := container.NewService(context.Background(), option.WithTokenSource(config.TokenSource))
containerService, err := container.NewService(context.Background(), clientOpts...)
if err != nil {
return nil, err
}
Expand Down
76 changes: 76 additions & 0 deletions providers/gce/gce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ package gce

import (
"context"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"

cloudprovider "k8s.io/cloud-provider"
)
Expand Down Expand Up @@ -496,6 +500,78 @@ func TestGenerateCloudConfigs(t *testing.T) {
}
}

// optionTypeName returns the unexported type name of a ClientOption using reflection.
func optionTypeName(opt option.ClientOption) string {
t := reflect.TypeOf(opt)
if t.Kind() == reflect.Ptr {
return "*" + t.Elem().Name()
}
return t.Name()
}

// writeFakeCredentials writes a minimal service account JSON to a temp file
// and sets GOOGLE_APPLICATION_CREDENTIALS to point at it.
func writeFakeCredentials(t *testing.T) {
t.Helper()
fakeJSON := `{
"type": "service_account",
"project_id": "test-project",
"private_key_id": "key-id",
"private_key": "fake-key",
"client_email": "test@test-project.iam.gserviceaccount.com",
"client_id": "123456789",
"token_uri": "https://oauth2.googleapis.com/token"
}`
f := filepath.Join(t.TempDir(), "creds.json")
if err := os.WriteFile(f, []byte(fakeJSON), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", f)
}

func TestClientOptions(t *testing.T) {
tests := []struct {
name string
ts oauth2.TokenSource
wantOptTypes []string
}{
{
name: "custom token source uses WithTokenSource",
ts: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "test"}),
wantOptTypes: []string{
"withTokenSource",
},
},
{
name: "JSON credentials uses WithAuthCredentialsJSON",
wantOptTypes: []string{
"withAuthCredentialsJSON",
"withUniverseDomain",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writeFakeCredentials(t)

opts, err := clientOptions(tt.ts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(opts) != len(tt.wantOptTypes) {
t.Fatalf("got %d options, want %d", len(opts), len(tt.wantOptTypes))
}
for i, wantType := range tt.wantOptTypes {
if got := optionTypeName(opts[i]); got != wantType {
t.Errorf("opts[%d] type = %s, want %s", i, got, wantType)
}
}
})
}
}

func TestNewAlphaFeatureGate(t *testing.T) {
testCases := []struct {
alphaFeatures []string
Expand Down