From 9f36f8f01ffd5440b3864ebc08856695136dd9e5 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 11:13:14 +0200 Subject: [PATCH] Adds support for using google service account credentials in the catalog provider gcp module Signed-off-by: Raghunandan Balachandran --- plugins/catalog-backend-module-gcp/README.md | 24 +++ .../catalog-backend-module-gcp/config.d.ts | 6 + .../src/providers/GkeEntityProvider.test.ts | 169 ++++++++++++++++++ .../src/providers/GkeEntityProvider.ts | 29 ++- 4 files changed, 227 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-gcp/README.md b/plugins/catalog-backend-module-gcp/README.md index d1ef926310..87c6ff5f6c 100644 --- a/plugins/catalog-backend-module-gcp/README.md +++ b/plugins/catalog-backend-module-gcp/README.md @@ -2,6 +2,15 @@ This is an extension module to the plugin-catalog-backend plugin, containing catalog processors and providers to ingest GCP resources as `Resource` kind entities. +## Authentication + +The GKE Entity Provider supports two authentication methods: + +1. **Service Account Credentials** (recommended for production): Provide Google Service Account credentials directly in the configuration +2. **Application Default Credentials**: If no credentials are provided, the provider falls back to: + - `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to a service account key file + - Google Cloud SDK default credentials (when running on Google Cloud Platform) + ## installation Register the plugin in `catalog.ts`` @@ -38,4 +47,19 @@ catalog: frequency: { minutes: 30 } # supports ISO duration, "human duration" as used in code timeout: { minutes: 3 } + # Optional: Google Service Account credentials for authentication + # If not provided, falls back to Application Default Credentials or GOOGLE_APPLICATION_CREDENTIALS + 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/..." + } ``` diff --git a/plugins/catalog-backend-module-gcp/config.d.ts b/plugins/catalog-backend-module-gcp/config.d.ts index dd657420f7..2ad369ef89 100644 --- a/plugins/catalog-backend-module-gcp/config.d.ts +++ b/plugins/catalog-backend-module-gcp/config.d.ts @@ -38,6 +38,12 @@ export interface Config { * (Optional) TaskScheduleDefinition for the refresh. */ schedule: SchedulerServiceTaskScheduleDefinitionConfig; + /** + * (Optional) Google Service Account credentials for authentication + * JSON string containing the service account key + * @visibility secret + */ + googleServiceAccountCredentials?: string; }; }; }; diff --git a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts index b9eb4cf2aa..da34e46110 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts @@ -19,6 +19,13 @@ import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; import * as container from '@google-cloud/container'; import { ConfigReader } from '@backstage/config'; +// Mock the container module +jest.mock('@google-cloud/container', () => ({ + v1: { + ClusterManagerClient: jest.fn(), + }, +})); + describe('GkeEntityProvider', () => { const clusterManagerClientMock = { listClusters: jest.fn(), @@ -197,4 +204,166 @@ describe('GkeEntityProvider', () => { expect(connectionMock.applyMutation).toHaveBeenCalledTimes(0); expect(logger.error).toHaveBeenCalledTimes(1); }); + + describe('credentials support', () => { + const MockedClusterManagerClient = container.v1 + .ClusterManagerClient as jest.MockedClass< + typeof container.v1.ClusterManagerClient + >; + + beforeEach(() => { + jest.resetAllMocks(); + MockedClusterManagerClient.mockClear(); + schedulerMock.createScheduledTaskRunner.mockReturnValue(taskRunner); + }); + + it('should use credentials from config when provided', () => { + const mockCredentials = { + 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: '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/test%40test-project.iam.gserviceaccount.com', + }; + + GkeEntityProvider.fromConfig({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/test-project/locations/-'], + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + googleServiceAccountCredentials: + JSON.stringify(mockCredentials), + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + + expect(MockedClusterManagerClient).toHaveBeenCalledWith({ + credentials: mockCredentials, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + }); + + it('should fall back to default credentials when no credentials provided', () => { + GkeEntityProvider.fromConfig({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/test-project/locations/-'], + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + // No googleServiceAccountCredentials provided + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); + }); + + it('should throw error for invalid JSON credentials', () => { + expect(() => { + GkeEntityProvider.fromConfig({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/test-project/locations/-'], + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + googleServiceAccountCredentials: 'invalid-json', + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + }).toThrow( + 'Failed to parse Google Service Account credentials from config:', + ); + }); + + it('should throw error for malformed JSON credentials', () => { + expect(() => { + GkeEntityProvider.fromConfig({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/test-project/locations/-'], + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + googleServiceAccountCredentials: '{"incomplete": "json"', + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + }).toThrow( + 'Failed to parse Google Service Account credentials from config:', + ); + }); + + it('should handle undefined credentials as fallback to default', () => { + GkeEntityProvider.fromConfig({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/test-project/locations/-'], + schedule: { + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }, + // googleServiceAccountCredentials is undefined + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + + expect(MockedClusterManagerClient).toHaveBeenCalledWith(); + }); + }); }); diff --git a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts index 13bf245f4e..58acf19870 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts @@ -71,11 +71,38 @@ export class GkeEntityProvider implements EntityProvider { scheduler: SchedulerService; config: Config; }) { + const gkeProviderConfig = config.getConfig('catalog.providers.gcp.gke'); + const credentials = gkeProviderConfig.getOptionalString( + 'googleServiceAccountCredentials', + ); + + let clusterManagerClient: container.v1.ClusterManagerClient; + + if (credentials && credentials.trim()) { + // Use credentials from config + try { + const credentialsObject = JSON.parse(credentials); + clusterManagerClient = 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 + clusterManagerClient = new container.v1.ClusterManagerClient(); + } + return GkeEntityProvider.fromConfigWithClient({ logger, scheduler: scheduler, config, - clusterManagerClient: new container.v1.ClusterManagerClient(), + clusterManagerClient, }); }