From 1bd09be1e44b3806d0cc7abda7baaae8fc037e13 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Thu, 7 Aug 2025 10:29:32 +0200 Subject: [PATCH 01/10] Add a googleServiceAccountCredentials json config. Uses the credentials if present else fallback to default application credentials Signed-off-by: Raghunandan Balachandran --- plugins/kubernetes-backend/config.d.ts | 7 +++ .../src/auth/GoogleServiceAccountStrategy.ts | 55 ++++++++++++++++++- .../src/service/KubernetesBuilder.ts | 2 +- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index a710b8cb85..74b2f57a31 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -98,6 +98,13 @@ export interface Config { plural: string; }>; + /** + * (Optional) Google Service Account credentials for authentication + * JSON string containing the service account key + * @visibility secret + */ + googleServiceAccountCredentials?: string; + /** * (Optional) API Version Overrides * If set, the specified api version will be used to make requests for the corresponding object. diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index f6757d6a64..4dce97ea29 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -20,19 +20,70 @@ import { KubernetesCredential, } from '@backstage/plugin-kubernetes-node'; import * as container from '@google-cloud/container'; +import { Config } from '@backstage/config'; /** + * GoogleServiceAccountStrategy provides authentication using Google Service Account credentials. + * + * Credentials can be provided via configuration: + * ```yaml + * kubernetes: + * googleServiceAccountCredentials: | + * { + * "type": "service_account", + * "project_id": "your-project-id", + * "private_key_id": "key-id", + * "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", + * "client_email": "your-service-account@your-project.iam.gserviceaccount.com", + * "client_id": "client-id", + * "auth_uri": "https://accounts.google.com/o/oauth2/auth", + * "token_uri": "https://oauth2.googleapis.com/token", + * "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + * "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..." + * } + * ``` + * + * If no credentials are provided in config, falls back to GOOGLE_APPLICATION_CREDENTIALS or ADC. * * @public */ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { + private readonly credentials?: string; + + constructor(config: Config) { + this.credentials = config.getOptionalString( + 'kubernetes.googleServiceAccountCredentials', + ); + } public async getCredential(): Promise { - const client = new container.v1.ClusterManagerClient(); + let client: container.v1.ClusterManagerClient; + + if (this.credentials) { + // Use credentials from config + try { + const credentialsObject = JSON.parse(this.credentials); + + client = new container.v1.ClusterManagerClient({ + credentials: credentialsObject, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + } catch (error) { + throw new Error( + `Failed to parse Google Service Account credentials from config: ${ + error instanceof Error ? error.message : 'Invalid JSON' + }`, + ); + } + } else { + // Fall back to Application Default Credentials or GOOGLE_APPLICATION_CREDENTIALS + client = new container.v1.ClusterManagerClient(); + } + const token = await client.auth.getAccessToken(); if (!token) { throw new Error( - 'Unable to obtain access token for the current Google Application Default Credentials', + 'Unable to obtain access token for Google Cloud authentication. Check your credentials configuration.', ); } return { type: 'bearer token', token }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index f86dd6cebb..9f87514eb4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -462,7 +462,7 @@ export class KubernetesBuilder { aws: new AwsIamStrategy({ config: this.env.config }), azure: new AzureIdentityStrategy(this.env.logger), google: new GoogleStrategy(), - googleServiceAccount: new GoogleServiceAccountStrategy(), + googleServiceAccount: new GoogleServiceAccountStrategy(this.env.config), localKubectlProxy: new AnonymousStrategy(), oidc: new OidcStrategy(), serviceAccount: new ServiceAccountStrategy(), From 35023cf5ccea8f16cd2ef109c93761a75de4b37d Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 11:12:06 +0200 Subject: [PATCH 02/10] Add docs and tests for k8s plugin changes Signed-off-by: Raghunandan Balachandran --- plugins/kubernetes-backend/TESTING_SETUP.md | 252 +++++++++++++++++ .../auth/GoogleServiceAccountStrategy.test.ts | 266 ++++++++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 plugins/kubernetes-backend/TESTING_SETUP.md create mode 100644 plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts diff --git a/plugins/kubernetes-backend/TESTING_SETUP.md b/plugins/kubernetes-backend/TESTING_SETUP.md new file mode 100644 index 0000000000..071ba71877 --- /dev/null +++ b/plugins/kubernetes-backend/TESTING_SETUP.md @@ -0,0 +1,252 @@ +# Testing GoogleServiceAccountStrategy with GKE + +This guide will help you set up a GKE cluster and service account to test the `GoogleServiceAccountStrategy`. + +## Prerequisites + +1. Install the Google Cloud CLI (`gcloud`) +2. A Google Cloud Project with billing enabled +3. Enable required APIs + +## Step 1: Enable Required APIs + +```bash +# Enable the required Google Cloud APIs +gcloud services enable container.googleapis.com +gcloud services enable compute.googleapis.com +gcloud services enable iam.googleapis.com +``` + +## Step 2: Create a GKE Cluster + +```bash +# Set your project ID +export PROJECT_ID="your-project-id" +export CLUSTER_NAME="backstage-test-cluster" +export REGION="us-central1" + +# Create a GKE cluster +gcloud container clusters create $CLUSTER_NAME \ + --region=$REGION \ + --project=$PROJECT_ID \ + --num-nodes=1 \ + --machine-type=e2-medium \ + --disk-size=20GB \ + --enable-autorepair \ + --enable-autoupgrade \ + --workload-pool=$PROJECT_ID.svc.id.goog +``` + +## Step 3: Create a Service Account for Backstage + +```bash +# Create a service account for Backstage +export SERVICE_ACCOUNT_NAME="backstage-k8s-reader" +export SERVICE_ACCOUNT_EMAIL="$SERVICE_ACCOUNT_NAME@$PROJECT_ID.iam.gserviceaccount.com" + +gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \ + --display-name="Backstage Kubernetes Reader" \ + --description="Service account for Backstage to access Kubernetes clusters" +``` + +## Step 4: Grant Required Permissions + +```bash +# Grant Container Engine Viewer role (to list and view clusters) +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ + --role="roles/container.clusterViewer" + +# Grant Kubernetes Engine Viewer role (to view cluster resources) +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ + --role="roles/container.viewer" + +# Optional: Grant more specific permissions if needed +# For reading pods, services, etc. +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ + --role="roles/container.developer" +``` + +## Step 5: Create and Download Service Account Key + +```bash +# Create a service account key +gcloud iam service-accounts keys create backstage-sa-key.json \ + --iam-account=$SERVICE_ACCOUNT_EMAIL + +# Display the key content (you'll need this for your Backstage config) +cat backstage-sa-key.json +``` + +## Step 6: Configure Backstage + +Add the following to your `app-config.yaml`: + +```yaml +kubernetes: + # Your service account credentials as a JSON string + googleServiceAccountCredentials: | + { + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "...", + "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", + "client_email": "backstage-k8s-reader@your-project-id.iam.gserviceaccount.com", + "client_id": "...", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..." + } + + clusterLocatorMethods: + - type: 'gke' + projectId: 'your-project-id' + region: 'us-central1' + authProvider: 'googleServiceAccount' +``` + +## Step 7: Test the Connection + +### Option 1: Using gcloud to verify cluster access + +```bash +# Get cluster credentials +gcloud container clusters get-credentials $CLUSTER_NAME \ + --region=$REGION \ + --project=$PROJECT_ID + +# Test kubectl access +kubectl get nodes +kubectl get pods --all-namespaces +``` + +### Option 2: Test with the service account directly + +```bash +# Activate the service account +gcloud auth activate-service-account $SERVICE_ACCOUNT_EMAIL \ + --key-file=backstage-sa-key.json + +# Get cluster credentials using the service account +gcloud container clusters get-credentials $CLUSTER_NAME \ + --region=$REGION \ + --project=$PROJECT_ID + +# Test access +kubectl get nodes +``` + +## Step 8: Deploy Test Workloads (Optional) + +```bash +# Create a test namespace +kubectl create namespace backstage-test + +# Deploy a simple nginx pod +kubectl create deployment nginx --image=nginx -n backstage-test + +# Create a service +kubectl expose deployment nginx --port=80 --type=ClusterIP -n backstage-test + +# Verify resources +kubectl get all -n backstage-test +``` + +## Step 9: Verify in Backstage + +1. Start your Backstage application +2. Navigate to the Kubernetes plugin +3. You should see your GKE cluster and its resources +4. Check the browser console and backend logs for any authentication errors + +## Troubleshooting + +### Common Issues + +1. **Permission Denied Errors** + + - Verify the service account has the correct IAM roles + - Check that the Kubernetes RBAC permissions are set correctly + +2. **Authentication Errors** + + - Ensure the service account key JSON is valid + - Verify the `googleServiceAccountCredentials` config is properly formatted + +3. **Cluster Not Found** + + - Check that the `projectId` and `region` match your actual cluster + - Verify the cluster exists: `gcloud container clusters list` + +4. **API Not Enabled** + - Ensure all required APIs are enabled in your project + +### Useful Commands + +```bash +# List all clusters +gcloud container clusters list + +# Get cluster info +gcloud container clusters describe $CLUSTER_NAME --region=$REGION + +# List service accounts +gcloud iam service-accounts list + +# Check IAM policies +gcloud projects get-iam-policy $PROJECT_ID + +# Test authentication with service account +gcloud auth list +``` + +## Cleanup + +When you're done testing: + +```bash +# Delete the cluster +gcloud container clusters delete $CLUSTER_NAME --region=$REGION + +# Delete the service account +gcloud iam service-accounts delete $SERVICE_ACCOUNT_EMAIL + +# Remove the key file +rm backstage-sa-key.json +``` + +## Security Best Practices + +1. **Never commit service account keys to version control** +2. **Use environment variables or secret management systems in production** +3. **Regularly rotate service account keys** +4. **Apply principle of least privilege - only grant necessary permissions** +5. **Monitor service account usage in Cloud Logging** + +## Alternative: Using Workload Identity (Recommended for Production) + +Instead of service account keys, consider using Workload Identity for production deployments: + +```bash +# Enable Workload Identity on the cluster +gcloud container clusters update $CLUSTER_NAME \ + --region=$REGION \ + --workload-pool=$PROJECT_ID.svc.id.goog + +# Create a Kubernetes service account +kubectl create serviceaccount backstage-ksa + +# Bind the Kubernetes service account to the Google service account +gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:$PROJECT_ID.svc.id.goog[default/backstage-ksa]" + +# Annotate the Kubernetes service account +kubectl annotate serviceaccount backstage-ksa \ + iam.gke.io/gcp-service-account=$SERVICE_ACCOUNT_EMAIL +``` + +This approach eliminates the need for service account keys and is more secure for production use. diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts new file mode 100644 index 0000000000..26ca1187e9 --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts @@ -0,0 +1,266 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +import { ConfigReader } from '@backstage/config'; +import { GoogleServiceAccountStrategy } from './GoogleServiceAccountStrategy'; + +// Mock the @google-cloud/container module +const mockGetAccessToken = jest.fn(); +const mockClusterManagerClient = jest.fn().mockImplementation(() => ({ + auth: { + getAccessToken: mockGetAccessToken, + }, +})); + +jest.mock('@google-cloud/container', () => ({ + v1: { + ClusterManagerClient: mockClusterManagerClient, + }, +})); + +describe('GoogleServiceAccountStrategy', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should read credentials from config when provided', () => { + const config = new ConfigReader({ + kubernetes: { + googleServiceAccountCredentials: '{"type": "service_account"}', + }, + }); + + const strategy = new GoogleServiceAccountStrategy(config); + expect(strategy).toBeDefined(); + }); + + it('should work without credentials in config', () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + const strategy = new GoogleServiceAccountStrategy(config); + expect(strategy).toBeDefined(); + }); + + it('should work with empty config', () => { + const config = new ConfigReader({}); + + const strategy = new GoogleServiceAccountStrategy(config); + expect(strategy).toBeDefined(); + }); + }); + + describe('#getCredential', () => { + it('should use credentials from config when provided', async () => { + const serviceAccountKey = { + type: 'service_account', + project_id: 'test-project', + private_key_id: 'key-id', + private_key: + '-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----\n', + client_email: 'test@test-project.iam.gserviceaccount.com', + client_id: '123456789', + auth_uri: 'https://accounts.google.com/o/oauth2/auth', + token_uri: 'https://oauth2.googleapis.com/token', + }; + + const config = new ConfigReader({ + kubernetes: { + googleServiceAccountCredentials: JSON.stringify(serviceAccountKey), + }, + }); + + mockGetAccessToken.mockResolvedValue('test-access-token'); + + const strategy = new GoogleServiceAccountStrategy(config); + const credential = await strategy.getCredential(); + + expect(mockClusterManagerClient).toHaveBeenCalledWith({ + credentials: serviceAccountKey, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + expect(mockGetAccessToken).toHaveBeenCalled(); + expect(credential).toEqual({ + type: 'bearer token', + token: 'test-access-token', + }); + }); + + it('should fall back to default credentials when no config provided', async () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + mockGetAccessToken.mockResolvedValue('default-access-token'); + + const strategy = new GoogleServiceAccountStrategy(config); + const credential = await strategy.getCredential(); + + expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(mockGetAccessToken).toHaveBeenCalled(); + expect(credential).toEqual({ + type: 'bearer token', + token: 'default-access-token', + }); + }); + + it('should throw error when JSON parsing fails', async () => { + const config = new ConfigReader({ + kubernetes: { + googleServiceAccountCredentials: 'invalid-json', + }, + }); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Failed to parse Google Service Account credentials from config', + ); + + expect(mockClusterManagerClient).not.toHaveBeenCalled(); + expect(mockGetAccessToken).not.toHaveBeenCalled(); + }); + + it('should throw error when access token is null', async () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + mockGetAccessToken.mockResolvedValue(null); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Unable to obtain access token for Google Cloud authentication', + ); + + expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(mockGetAccessToken).toHaveBeenCalled(); + }); + + it('should throw error when access token is undefined', async () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + mockGetAccessToken.mockResolvedValue(undefined); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Unable to obtain access token for Google Cloud authentication', + ); + + expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(mockGetAccessToken).toHaveBeenCalled(); + }); + + it('should handle empty string access token', async () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + mockGetAccessToken.mockResolvedValue(''); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Unable to obtain access token for Google Cloud authentication', + ); + + expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(mockGetAccessToken).toHaveBeenCalled(); + }); + + it('should handle malformed JSON with specific error message', async () => { + const config = new ConfigReader({ + kubernetes: { + googleServiceAccountCredentials: '{"invalid": json}', + }, + }); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + /Failed to parse Google Service Account credentials from config: Unexpected token/, + ); + }); + + it('should handle client creation errors', async () => { + const serviceAccountKey = { + type: 'service_account', + project_id: 'test-project', + }; + + const config = new ConfigReader({ + kubernetes: { + googleServiceAccountCredentials: JSON.stringify(serviceAccountKey), + }, + }); + + mockClusterManagerClient.mockImplementationOnce(() => { + throw new Error('Client creation failed'); + }); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Client creation failed', + ); + }); + + it('should handle getAccessToken errors', async () => { + const config = new ConfigReader({ + kubernetes: {}, + }); + + mockGetAccessToken.mockRejectedValue(new Error('Token fetch failed')); + + const strategy = new GoogleServiceAccountStrategy(config); + + await expect(strategy.getCredential()).rejects.toThrow( + 'Token fetch failed', + ); + + expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(mockGetAccessToken).toHaveBeenCalled(); + }); + }); + + describe('#validateCluster', () => { + it('should return empty array', () => { + const config = new ConfigReader({}); + const strategy = new GoogleServiceAccountStrategy(config); + + const result = strategy.validateCluster(); + + expect(result).toEqual([]); + }); + }); + + describe('#presentAuthMetadata', () => { + it('should return empty object', () => { + const config = new ConfigReader({}); + const strategy = new GoogleServiceAccountStrategy(config); + + const result = strategy.presentAuthMetadata({ test: 'metadata' }); + + expect(result).toEqual({}); + }); + }); +}); From 5f424c63b325a0059fa0efa5feaaad37ba705cb4 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 15:58:53 +0200 Subject: [PATCH 03/10] Add changeset Signed-off-by: Raghunandan Balachandran --- .changeset/spicy-dragons-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spicy-dragons-act.md diff --git a/.changeset/spicy-dragons-act.md b/.changeset/spicy-dragons-act.md new file mode 100644 index 0000000000..d207250afa --- /dev/null +++ b/.changeset/spicy-dragons-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Added support for 'googleServiceAccountCredential' configuration parameter to use GoogleServiceAccountStrategy From b48d9f21026fa5a98d45a24f291d8b8dac012c3b Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 16:22:00 +0200 Subject: [PATCH 04/10] remove testing docs Signed-off-by: Raghunandan Balachandran --- plugins/kubernetes-backend/TESTING_SETUP.md | 252 -------------------- 1 file changed, 252 deletions(-) delete mode 100644 plugins/kubernetes-backend/TESTING_SETUP.md diff --git a/plugins/kubernetes-backend/TESTING_SETUP.md b/plugins/kubernetes-backend/TESTING_SETUP.md deleted file mode 100644 index 071ba71877..0000000000 --- a/plugins/kubernetes-backend/TESTING_SETUP.md +++ /dev/null @@ -1,252 +0,0 @@ -# Testing GoogleServiceAccountStrategy with GKE - -This guide will help you set up a GKE cluster and service account to test the `GoogleServiceAccountStrategy`. - -## Prerequisites - -1. Install the Google Cloud CLI (`gcloud`) -2. A Google Cloud Project with billing enabled -3. Enable required APIs - -## Step 1: Enable Required APIs - -```bash -# Enable the required Google Cloud APIs -gcloud services enable container.googleapis.com -gcloud services enable compute.googleapis.com -gcloud services enable iam.googleapis.com -``` - -## Step 2: Create a GKE Cluster - -```bash -# Set your project ID -export PROJECT_ID="your-project-id" -export CLUSTER_NAME="backstage-test-cluster" -export REGION="us-central1" - -# Create a GKE cluster -gcloud container clusters create $CLUSTER_NAME \ - --region=$REGION \ - --project=$PROJECT_ID \ - --num-nodes=1 \ - --machine-type=e2-medium \ - --disk-size=20GB \ - --enable-autorepair \ - --enable-autoupgrade \ - --workload-pool=$PROJECT_ID.svc.id.goog -``` - -## Step 3: Create a Service Account for Backstage - -```bash -# Create a service account for Backstage -export SERVICE_ACCOUNT_NAME="backstage-k8s-reader" -export SERVICE_ACCOUNT_EMAIL="$SERVICE_ACCOUNT_NAME@$PROJECT_ID.iam.gserviceaccount.com" - -gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \ - --display-name="Backstage Kubernetes Reader" \ - --description="Service account for Backstage to access Kubernetes clusters" -``` - -## Step 4: Grant Required Permissions - -```bash -# Grant Container Engine Viewer role (to list and view clusters) -gcloud projects add-iam-policy-binding $PROJECT_ID \ - --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ - --role="roles/container.clusterViewer" - -# Grant Kubernetes Engine Viewer role (to view cluster resources) -gcloud projects add-iam-policy-binding $PROJECT_ID \ - --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ - --role="roles/container.viewer" - -# Optional: Grant more specific permissions if needed -# For reading pods, services, etc. -gcloud projects add-iam-policy-binding $PROJECT_ID \ - --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ - --role="roles/container.developer" -``` - -## Step 5: Create and Download Service Account Key - -```bash -# Create a service account key -gcloud iam service-accounts keys create backstage-sa-key.json \ - --iam-account=$SERVICE_ACCOUNT_EMAIL - -# Display the key content (you'll need this for your Backstage config) -cat backstage-sa-key.json -``` - -## Step 6: Configure Backstage - -Add the following to your `app-config.yaml`: - -```yaml -kubernetes: - # Your service account credentials as a JSON string - googleServiceAccountCredentials: | - { - "type": "service_account", - "project_id": "your-project-id", - "private_key_id": "...", - "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", - "client_email": "backstage-k8s-reader@your-project-id.iam.gserviceaccount.com", - "client_id": "...", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..." - } - - clusterLocatorMethods: - - type: 'gke' - projectId: 'your-project-id' - region: 'us-central1' - authProvider: 'googleServiceAccount' -``` - -## Step 7: Test the Connection - -### Option 1: Using gcloud to verify cluster access - -```bash -# Get cluster credentials -gcloud container clusters get-credentials $CLUSTER_NAME \ - --region=$REGION \ - --project=$PROJECT_ID - -# Test kubectl access -kubectl get nodes -kubectl get pods --all-namespaces -``` - -### Option 2: Test with the service account directly - -```bash -# Activate the service account -gcloud auth activate-service-account $SERVICE_ACCOUNT_EMAIL \ - --key-file=backstage-sa-key.json - -# Get cluster credentials using the service account -gcloud container clusters get-credentials $CLUSTER_NAME \ - --region=$REGION \ - --project=$PROJECT_ID - -# Test access -kubectl get nodes -``` - -## Step 8: Deploy Test Workloads (Optional) - -```bash -# Create a test namespace -kubectl create namespace backstage-test - -# Deploy a simple nginx pod -kubectl create deployment nginx --image=nginx -n backstage-test - -# Create a service -kubectl expose deployment nginx --port=80 --type=ClusterIP -n backstage-test - -# Verify resources -kubectl get all -n backstage-test -``` - -## Step 9: Verify in Backstage - -1. Start your Backstage application -2. Navigate to the Kubernetes plugin -3. You should see your GKE cluster and its resources -4. Check the browser console and backend logs for any authentication errors - -## Troubleshooting - -### Common Issues - -1. **Permission Denied Errors** - - - Verify the service account has the correct IAM roles - - Check that the Kubernetes RBAC permissions are set correctly - -2. **Authentication Errors** - - - Ensure the service account key JSON is valid - - Verify the `googleServiceAccountCredentials` config is properly formatted - -3. **Cluster Not Found** - - - Check that the `projectId` and `region` match your actual cluster - - Verify the cluster exists: `gcloud container clusters list` - -4. **API Not Enabled** - - Ensure all required APIs are enabled in your project - -### Useful Commands - -```bash -# List all clusters -gcloud container clusters list - -# Get cluster info -gcloud container clusters describe $CLUSTER_NAME --region=$REGION - -# List service accounts -gcloud iam service-accounts list - -# Check IAM policies -gcloud projects get-iam-policy $PROJECT_ID - -# Test authentication with service account -gcloud auth list -``` - -## Cleanup - -When you're done testing: - -```bash -# Delete the cluster -gcloud container clusters delete $CLUSTER_NAME --region=$REGION - -# Delete the service account -gcloud iam service-accounts delete $SERVICE_ACCOUNT_EMAIL - -# Remove the key file -rm backstage-sa-key.json -``` - -## Security Best Practices - -1. **Never commit service account keys to version control** -2. **Use environment variables or secret management systems in production** -3. **Regularly rotate service account keys** -4. **Apply principle of least privilege - only grant necessary permissions** -5. **Monitor service account usage in Cloud Logging** - -## Alternative: Using Workload Identity (Recommended for Production) - -Instead of service account keys, consider using Workload Identity for production deployments: - -```bash -# Enable Workload Identity on the cluster -gcloud container clusters update $CLUSTER_NAME \ - --region=$REGION \ - --workload-pool=$PROJECT_ID.svc.id.goog - -# Create a Kubernetes service account -kubectl create serviceaccount backstage-ksa - -# Bind the Kubernetes service account to the Google service account -gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \ - --role roles/iam.workloadIdentityUser \ - --member "serviceAccount:$PROJECT_ID.svc.id.goog[default/backstage-ksa]" - -# Annotate the Kubernetes service account -kubectl annotate serviceaccount backstage-ksa \ - iam.gke.io/gcp-service-account=$SERVICE_ACCOUNT_EMAIL -``` - -This approach eliminates the need for service account keys and is more secure for production use. From b050feb497776a09fda277337ff30754e4def66d Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 16:25:47 +0200 Subject: [PATCH 05/10] update changeset Signed-off-by: Raghunandan Balachandran --- .changeset/spicy-dragons-act.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spicy-dragons-act.md b/.changeset/spicy-dragons-act.md index d207250afa..091df63554 100644 --- a/.changeset/spicy-dragons-act.md +++ b/.changeset/spicy-dragons-act.md @@ -2,4 +2,4 @@ '@backstage/plugin-kubernetes-backend': patch --- -Added support for 'googleServiceAccountCredential' configuration parameter to use GoogleServiceAccountStrategy +Added support for Google Service account credentials config to use in GoogleServiceAccountStrategy From bd8046c1de7e208037bc4297da94ae41305da386 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 16:48:52 +0200 Subject: [PATCH 06/10] fix tests Signed-off-by: Raghunandan Balachandran --- .../auth/GoogleServiceAccountStrategy.test.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts index 26ca1187e9..dedebb9684 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts @@ -19,17 +19,25 @@ import { GoogleServiceAccountStrategy } from './GoogleServiceAccountStrategy'; // Mock the @google-cloud/container module const mockGetAccessToken = jest.fn(); -const mockClusterManagerClient = jest.fn().mockImplementation(() => ({ - auth: { - getAccessToken: mockGetAccessToken, - }, -})); -jest.mock('@google-cloud/container', () => ({ - v1: { - ClusterManagerClient: mockClusterManagerClient, - }, -})); +jest.mock('@google-cloud/container', () => { + const mockClusterManagerClient = jest.fn().mockImplementation(() => ({ + auth: { + getAccessToken: mockGetAccessToken, + }, + })); + + return { + v1: { + ClusterManagerClient: mockClusterManagerClient, + }, + }; +}); + +// Get reference to the mocked constructor for use in tests +const { + v1: { ClusterManagerClient: MockedClusterManagerClient }, +} = jest.mocked(require('@google-cloud/container')); describe('GoogleServiceAccountStrategy', () => { beforeEach(() => { @@ -90,7 +98,7 @@ describe('GoogleServiceAccountStrategy', () => { const strategy = new GoogleServiceAccountStrategy(config); const credential = await strategy.getCredential(); - expect(mockClusterManagerClient).toHaveBeenCalledWith({ + expect(MockedClusterManagerClient).toHaveBeenCalledWith({ credentials: serviceAccountKey, scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); @@ -111,7 +119,7 @@ describe('GoogleServiceAccountStrategy', () => { const strategy = new GoogleServiceAccountStrategy(config); const credential = await strategy.getCredential(); - expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); expect(mockGetAccessToken).toHaveBeenCalled(); expect(credential).toEqual({ type: 'bearer token', @@ -132,7 +140,7 @@ describe('GoogleServiceAccountStrategy', () => { 'Failed to parse Google Service Account credentials from config', ); - expect(mockClusterManagerClient).not.toHaveBeenCalled(); + expect(MockedClusterManagerClient).not.toHaveBeenCalled(); expect(mockGetAccessToken).not.toHaveBeenCalled(); }); @@ -149,7 +157,7 @@ describe('GoogleServiceAccountStrategy', () => { 'Unable to obtain access token for Google Cloud authentication', ); - expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); expect(mockGetAccessToken).toHaveBeenCalled(); }); @@ -166,7 +174,7 @@ describe('GoogleServiceAccountStrategy', () => { 'Unable to obtain access token for Google Cloud authentication', ); - expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); expect(mockGetAccessToken).toHaveBeenCalled(); }); @@ -183,7 +191,7 @@ describe('GoogleServiceAccountStrategy', () => { 'Unable to obtain access token for Google Cloud authentication', ); - expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); expect(mockGetAccessToken).toHaveBeenCalled(); }); @@ -213,7 +221,7 @@ describe('GoogleServiceAccountStrategy', () => { }, }); - mockClusterManagerClient.mockImplementationOnce(() => { + MockedClusterManagerClient.mockImplementationOnce(() => { throw new Error('Client creation failed'); }); @@ -237,7 +245,7 @@ describe('GoogleServiceAccountStrategy', () => { 'Token fetch failed', ); - expect(mockClusterManagerClient).toHaveBeenCalledWith(); + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); expect(mockGetAccessToken).toHaveBeenCalled(); }); }); From 01d7a497aad595d95d2ff028c582dedc710c8008 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 17:09:57 +0200 Subject: [PATCH 07/10] update api reports Signed-off-by: Raghunandan Balachandran --- plugins/kubernetes-backend/report.api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/kubernetes-backend/report.api.md b/plugins/kubernetes-backend/report.api.md index dfc93b9da6..d16fc103a1 100644 --- a/plugins/kubernetes-backend/report.api.md +++ b/plugins/kubernetes-backend/report.api.md @@ -90,8 +90,12 @@ export type DispatchStrategyOptions = { }; }; -// @public (undocumented) +// @public @deprecated (undocumented) +export type FetchResponseWrapper = k8sAuthTypes.FetchResponseWrapper; + +// @public export class GoogleServiceAccountStrategy implements AuthenticationStrategy { + constructor(config: Config); // (undocumented) getCredential(): Promise; // (undocumented) From c005fbb3dc4f9ba83cfe3f205308682e4aa0345b Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 17:39:12 +0200 Subject: [PATCH 08/10] reduce scopes to gke container api Signed-off-by: Raghunandan Balachandran --- .../src/auth/GoogleServiceAccountStrategy.test.ts | 2 +- .../kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts index dedebb9684..57181f61c6 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts @@ -100,7 +100,7 @@ describe('GoogleServiceAccountStrategy', () => { expect(MockedClusterManagerClient).toHaveBeenCalledWith({ credentials: serviceAccountKey, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], + scopes: ['https://www.googleapis.com/auth/container.readonly'], }); expect(mockGetAccessToken).toHaveBeenCalled(); expect(credential).toEqual({ diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 4dce97ea29..a6ffdc00e8 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -65,7 +65,7 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { client = new container.v1.ClusterManagerClient({ credentials: credentialsObject, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], + scopes: ['https://www.googleapis.com/auth/container.readonly'], }); } catch (error) { throw new Error( From 02045fa299bd06b3d2774132c642860a7b0265a0 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Tue, 12 Aug 2025 09:54:46 +0200 Subject: [PATCH 09/10] remove explicit scopes Signed-off-by: Raghunandan Balachandran --- .../src/auth/GoogleServiceAccountStrategy.test.ts | 1 - .../kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts index 57181f61c6..d70cba784d 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts @@ -100,7 +100,6 @@ describe('GoogleServiceAccountStrategy', () => { expect(MockedClusterManagerClient).toHaveBeenCalledWith({ credentials: serviceAccountKey, - scopes: ['https://www.googleapis.com/auth/container.readonly'], }); expect(mockGetAccessToken).toHaveBeenCalled(); expect(credential).toEqual({ diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index a6ffdc00e8..2e2af018bc 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -65,7 +65,6 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { client = new container.v1.ClusterManagerClient({ credentials: credentialsObject, - scopes: ['https://www.googleapis.com/auth/container.readonly'], }); } catch (error) { throw new Error( From c014ad31668f34d771c50e2a51beb702643f8978 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Tue, 12 Aug 2025 16:29:18 +0200 Subject: [PATCH 10/10] consume config through options Signed-off-by: Raghunandan Balachandran --- plugins/kubernetes-backend/report.api.md | 5 +--- .../auth/GoogleServiceAccountStrategy.test.ts | 28 +++++++++---------- .../src/auth/GoogleServiceAccountStrategy.ts | 4 +-- .../src/service/KubernetesBuilder.ts | 4 ++- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/plugins/kubernetes-backend/report.api.md b/plugins/kubernetes-backend/report.api.md index d16fc103a1..798d5d7d29 100644 --- a/plugins/kubernetes-backend/report.api.md +++ b/plugins/kubernetes-backend/report.api.md @@ -90,12 +90,9 @@ export type DispatchStrategyOptions = { }; }; -// @public @deprecated (undocumented) -export type FetchResponseWrapper = k8sAuthTypes.FetchResponseWrapper; - // @public export class GoogleServiceAccountStrategy implements AuthenticationStrategy { - constructor(config: Config); + constructor(opts: { config: Config }); // (undocumented) getCredential(): Promise; // (undocumented) diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts index d70cba784d..34022556b8 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.test.ts @@ -52,7 +52,7 @@ describe('GoogleServiceAccountStrategy', () => { }, }); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); expect(strategy).toBeDefined(); }); @@ -61,14 +61,14 @@ describe('GoogleServiceAccountStrategy', () => { kubernetes: {}, }); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); expect(strategy).toBeDefined(); }); it('should work with empty config', () => { const config = new ConfigReader({}); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); expect(strategy).toBeDefined(); }); }); @@ -95,7 +95,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockResolvedValue('test-access-token'); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); const credential = await strategy.getCredential(); expect(MockedClusterManagerClient).toHaveBeenCalledWith({ @@ -115,7 +115,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockResolvedValue('default-access-token'); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); const credential = await strategy.getCredential(); expect(MockedClusterManagerClient).toHaveBeenCalledWith(); @@ -133,7 +133,7 @@ describe('GoogleServiceAccountStrategy', () => { }, }); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Failed to parse Google Service Account credentials from config', @@ -150,7 +150,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockResolvedValue(null); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Unable to obtain access token for Google Cloud authentication', @@ -167,7 +167,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockResolvedValue(undefined); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Unable to obtain access token for Google Cloud authentication', @@ -184,7 +184,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockResolvedValue(''); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Unable to obtain access token for Google Cloud authentication', @@ -201,7 +201,7 @@ describe('GoogleServiceAccountStrategy', () => { }, }); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( /Failed to parse Google Service Account credentials from config: Unexpected token/, @@ -224,7 +224,7 @@ describe('GoogleServiceAccountStrategy', () => { throw new Error('Client creation failed'); }); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Client creation failed', @@ -238,7 +238,7 @@ describe('GoogleServiceAccountStrategy', () => { mockGetAccessToken.mockRejectedValue(new Error('Token fetch failed')); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); await expect(strategy.getCredential()).rejects.toThrow( 'Token fetch failed', @@ -252,7 +252,7 @@ describe('GoogleServiceAccountStrategy', () => { describe('#validateCluster', () => { it('should return empty array', () => { const config = new ConfigReader({}); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); const result = strategy.validateCluster(); @@ -263,7 +263,7 @@ describe('GoogleServiceAccountStrategy', () => { describe('#presentAuthMetadata', () => { it('should return empty object', () => { const config = new ConfigReader({}); - const strategy = new GoogleServiceAccountStrategy(config); + const strategy = new GoogleServiceAccountStrategy({ config }); const result = strategy.presentAuthMetadata({ test: 'metadata' }); diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 2e2af018bc..6bb7f8d7ee 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -50,8 +50,8 @@ import { Config } from '@backstage/config'; export class GoogleServiceAccountStrategy implements AuthenticationStrategy { private readonly credentials?: string; - constructor(config: Config) { - this.credentials = config.getOptionalString( + constructor(opts: { config: Config }) { + this.credentials = opts.config.getOptionalString( 'kubernetes.googleServiceAccountCredentials', ); } diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 9f87514eb4..2cf0bcf04c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -462,7 +462,9 @@ export class KubernetesBuilder { aws: new AwsIamStrategy({ config: this.env.config }), azure: new AzureIdentityStrategy(this.env.logger), google: new GoogleStrategy(), - googleServiceAccount: new GoogleServiceAccountStrategy(this.env.config), + googleServiceAccount: new GoogleServiceAccountStrategy({ + config: this.env.config, + }), localKubectlProxy: new AnonymousStrategy(), oidc: new OidcStrategy(), serviceAccount: new ServiceAccountStrategy(),