Skip to content
Open
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
30 changes: 25 additions & 5 deletions pkg/addons/addons_gcpauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ func enableAddonGCPAuth(cfg *config.ClusterConfig, options *run.CommandOptions)
creds, err := google.FindDefaultCredentials(ctx)
if err != nil {
if detect.IsCloudShell() {
if c := os.Getenv("CLOUDSDK_CONFIG"); c != "" {
f, err := os.ReadFile(path.Join(c, "application_default_credentials.json"))
if err == nil {
creds, _ = google.CredentialsFromJSON(ctx, f) //nolint:staticcheck
}
var cloudShellErr error
creds, cloudShellErr = credentialsFromCloudShellADC(ctx)
if cloudShellErr != nil {
klog.Warningf("failed to load Cloud Shell credentials: %v", cloudShellErr)
}
} else {
exit.Message(reason.InternalCredsNotFound, "Could not find any GCP credentials. Either run `gcloud auth application-default login` or set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the path of your credentials file.")
Expand All @@ -98,6 +97,9 @@ func enableAddonGCPAuth(cfg *config.ClusterConfig, options *run.CommandOptions)
return nil
}

if creds == nil {
exit.Message(reason.InternalCredsNotFound, "Could not find any GCP credentials. Either run `gcloud auth application-default login` or set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the path of your credentials file.")
}
if creds.JSON == nil {
out.WarningT("You have authenticated with a service account that does not have an associated JSON file. The GCP Auth addon requires credentials with a JSON file in order to continue.")
return nil
Expand Down Expand Up @@ -137,6 +139,24 @@ or set the GOOGLE_CLOUD_PROJECT environment variable.`)

}

// credentialsFromCloudShellADC loads application default credentials from the
// Cloud Shell CLOUDSDK_CONFIG directory.
func credentialsFromCloudShellADC(ctx context.Context) (*google.Credentials, error) {
c := os.Getenv("CLOUDSDK_CONFIG")
if c == "" {
return nil, fmt.Errorf("CLOUDSDK_CONFIG is not set")
}
f, err := os.ReadFile(path.Join(c, "application_default_credentials.json"))
if err != nil {
return nil, fmt.Errorf("reading application default credentials: %w", err)
}
creds, err := google.CredentialsFromJSON(ctx, f) //nolint:staticcheck
if err != nil {
return nil, fmt.Errorf("parsing application default credentials: %w", err)
}
return creds, nil
}

func patchServiceAccounts(cc *config.ClusterConfig) error {
client, err := service.K8s.GetCoreClient(cc.Name)
if err != nil {
Expand Down
82 changes: 82 additions & 0 deletions pkg/addons/addons_gcpauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2024 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addons

import (
"context"
"os"
"path/filepath"
"testing"
)

// minimalAuthorizedUserJSON is a valid authorized_user JSON accepted by google.CredentialsFromJSON.
// authorized_user credentials do not require a cryptographic key, making them suitable for unit tests.
const minimalAuthorizedUserJSON = `{
"type": "authorized_user",
"client_id": "test-client-id.apps.googleusercontent.com",
"client_secret": "test-client-secret",
"refresh_token": "test-refresh-token"
}`

func TestCredentialsFromCloudShellADC_EnvUnset(t *testing.T) {
t.Setenv("CLOUDSDK_CONFIG", "")
_, err := credentialsFromCloudShellADC(context.Background())
if err == nil {
t.Fatal("expected error when CLOUDSDK_CONFIG is unset, got nil")
}
}

func TestCredentialsFromCloudShellADC_FileMissing(t *testing.T) {
dir := t.TempDir()
t.Setenv("CLOUDSDK_CONFIG", dir)
// No application_default_credentials.json written
_, err := credentialsFromCloudShellADC(context.Background())
if err == nil {
t.Fatal("expected error when credentials file is missing, got nil")
}
}

func TestCredentialsFromCloudShellADC_InvalidJSON(t *testing.T) {
dir := t.TempDir()
t.Setenv("CLOUDSDK_CONFIG", dir)
if err := os.WriteFile(filepath.Join(dir, "application_default_credentials.json"), []byte("not valid json"), 0600); err != nil {
t.Fatalf("writing test file: %v", err)
}
_, err := credentialsFromCloudShellADC(context.Background())
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}

func TestCredentialsFromCloudShellADC_ValidCredentials(t *testing.T) {
dir := t.TempDir()
t.Setenv("CLOUDSDK_CONFIG", dir)
data := []byte(minimalAuthorizedUserJSON)
if err := os.WriteFile(filepath.Join(dir, "application_default_credentials.json"), data, 0600); err != nil {
t.Fatalf("writing test file: %v", err)
}
creds, err := credentialsFromCloudShellADC(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if creds == nil {
t.Fatal("expected non-nil credentials, got nil")
}
if string(creds.JSON) != string(data) {
t.Errorf("creds.JSON does not match file contents\ngot: %s\nwant: %s", creds.JSON, data)
}
}