From 9f36f8f01ffd5440b3864ebc08856695136dd9e5 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 11:13:14 +0200 Subject: [PATCH 1/8] 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, }); } From 1c42a72179dbb3e7160ed0f368ad9b0e76c3d02f Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 13:25:56 +0200 Subject: [PATCH 2/8] set authProvider and owner to use for gke resources in config Signed-off-by: Raghunandan Balachandran --- plugins/catalog-backend-module-gcp/README.md | 7 ++ .../catalog-backend-module-gcp/config.d.ts | 10 +++ .../src/providers/GkeEntityProvider.test.ts | 75 +++++++++++++++++++ .../src/providers/GkeEntityProvider.ts | 13 +++- .../GkeEntityProvider.test.ts.snap | 44 +++++++++++ 5 files changed, 147 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-gcp/README.md b/plugins/catalog-backend-module-gcp/README.md index 87c6ff5f6c..0f9ed67576 100644 --- a/plugins/catalog-backend-module-gcp/README.md +++ b/plugins/catalog-backend-module-gcp/README.md @@ -62,4 +62,11 @@ catalog: "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..." } + # Optional: Authentication provider for Kubernetes clusters + # Defaults to 'google' if not specified + # Common values: 'google', 'googleServiceAccount' + authProvider: googleServiceAccount + # Optional: Owner of the discovered GKE clusters + # Defaults to 'unknown' if not specified + owner: platform-team ``` diff --git a/plugins/catalog-backend-module-gcp/config.d.ts b/plugins/catalog-backend-module-gcp/config.d.ts index 2ad369ef89..4cf1815980 100644 --- a/plugins/catalog-backend-module-gcp/config.d.ts +++ b/plugins/catalog-backend-module-gcp/config.d.ts @@ -44,6 +44,16 @@ export interface Config { * @visibility secret */ googleServiceAccountCredentials?: string; + /** + * (Optional) Authentication provider to use for Kubernetes clusters + * Defaults to 'google' for backward compatibility + */ + authProvider?: string; + /** + * (Optional) Owner of the discovered clusters + * Defaults to 'unknown' if not specified + */ + owner?: 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 da34e46110..dcd7d5c05e 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts @@ -196,6 +196,54 @@ describe('GkeEntityProvider', () => { }, ); + it('should use configured values for authProvider and owner', async () => { + const customGkeEntityProvider = GkeEntityProvider.fromConfigWithClient({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/parent1/locations/-'], + authProvider: 'googleServiceAccount', + owner: 'sre', + schedule: { + frequency: { + minutes: 3, + }, + timeout: { + minutes: 3, + }, + }, + }, + }, + }, + }, + }), + scheduler: schedulerMock, + clusterManagerClient: clusterManagerClientMock as any, + }); + + clusterManagerClientMock.listClusters.mockImplementation(() => [ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '127.0.0.1', + location: 'some-location', + selfLink: 'https://127.0.0.1/some-link', + masterAuth: { + clusterCaCertificate: 'abcdefg', + }, + }, + ], + }, + ]); + await customGkeEntityProvider.connect(connectionMock); + await customGkeEntityProvider.refresh(); + expect(connectionMock.applyMutation).toMatchSnapshot(); + }); + it('should log GKE API errors', async () => { clusterManagerClientMock.listClusters.mockRejectedValue( new Error('some-error'), @@ -365,5 +413,32 @@ describe('GkeEntityProvider', () => { expect(MockedClusterManagerClient).toHaveBeenCalledWith(); }); + + it('should read authProvider and owner from config', () => { + const provider = 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 }, + }, + authProvider: 'googleServiceAccount', + owner: 'platform-team', + }, + }, + }, + }, + }), + scheduler: schedulerMock, + }); + + expect(provider).toBeDefined(); + 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 58acf19870..a60dd717a5 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts @@ -47,6 +47,8 @@ export class GkeEntityProvider implements EntityProvider { private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private readonly gkeParents: string[]; + private readonly gkeAuthProvider: string | undefined; + private readonly gkeOwner: string | undefined; private readonly clusterManagerClient: container.v1.ClusterManagerClient; private connection?: EntityProviderConnection; @@ -54,11 +56,15 @@ export class GkeEntityProvider implements EntityProvider { logger: LoggerService, taskRunner: SchedulerServiceTaskRunner, gkeParents: string[], + gkeAuthProvider: string | undefined, + gkeOwner: string | undefined, clusterManagerClient: container.v1.ClusterManagerClient, ) { this.logger = logger; this.scheduleFn = this.createScheduleFn(taskRunner); this.gkeParents = gkeParents; + this.gkeAuthProvider = gkeAuthProvider; + this.gkeOwner = gkeOwner; this.clusterManagerClient = clusterManagerClient; } @@ -125,6 +131,8 @@ export class GkeEntityProvider implements EntityProvider { logger, scheduler.createScheduledTaskRunner(schedule), gkeProviderConfig.getStringArray('parents'), + gkeProviderConfig.getOptionalString('authProvider'), + gkeProviderConfig.getOptionalString('owner'), clusterManagerClient, ); } @@ -179,7 +187,8 @@ export class GkeEntityProvider implements EntityProvider { [ANNOTATION_KUBERNETES_API_SERVER]: `https://${cluster.endpoint}`, [ANNOTATION_KUBERNETES_API_SERVER_CA]: cluster.masterAuth?.clusterCaCertificate || '', - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: + this.gkeAuthProvider || 'google', [ANNOTATION_KUBERNETES_DASHBOARD_APP]: 'gke', [ANNOTATION_LOCATION]: location, [ANNOTATION_ORIGIN_LOCATION]: location, @@ -194,7 +203,7 @@ export class GkeEntityProvider implements EntityProvider { }, spec: { type: 'kubernetes-cluster', - owner: 'unknown', + owner: this.gkeOwner || 'unknown', }, }, }; diff --git a/plugins/catalog-backend-module-gcp/src/providers/__snapshots__/GkeEntityProvider.test.ts.snap b/plugins/catalog-backend-module-gcp/src/providers/__snapshots__/GkeEntityProvider.test.ts.snap index d20efb9c8d..c903bcd192 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/__snapshots__/GkeEntityProvider.test.ts.snap +++ b/plugins/catalog-backend-module-gcp/src/providers/__snapshots__/GkeEntityProvider.test.ts.snap @@ -67,3 +67,47 @@ exports[`GkeEntityProvider should return clusters as Resources 1`] = ` ], } `; + +exports[`GkeEntityProvider should use configured values for authProvider and owner 1`] = ` +[MockFunction] { + "calls": [ + [ + { + "entities": [ + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Resource", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "gcp-gke:some-location", + "backstage.io/managed-by-origin-location": "gcp-gke:some-location", + "kubernetes.io/api-server": "https://127.0.0.1", + "kubernetes.io/api-server-certificate-authority": "abcdefg", + "kubernetes.io/auth-provider": "googleServiceAccount", + "kubernetes.io/dashboard-app": "gke", + "kubernetes.io/dashboard-parameters": "{"projectId":"parent1","region":"some-location","clusterName":"some-cluster"}", + }, + "name": "some-cluster", + "namespace": "default", + }, + "spec": { + "owner": "sre", + "type": "kubernetes-cluster", + }, + }, + "locationKey": "gcp-gke:some-location", + }, + ], + "type": "full", + }, + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], +} +`; From cb36a867f1c0e582c75440689ec99967297b6969 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 15:54:51 +0200 Subject: [PATCH 3/8] Add changeset Signed-off-by: Raghunandan Balachandran --- .changeset/fluffy-bananas-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fluffy-bananas-shake.md diff --git a/.changeset/fluffy-bananas-shake.md b/.changeset/fluffy-bananas-shake.md new file mode 100644 index 0000000000..b866cd024c --- /dev/null +++ b/.changeset/fluffy-bananas-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gcp': patch +--- + +Added support for optional `authProvider` and `owner` configuration parameters for GkeEntityProvider From 261199e9fc94f13a5af438fba478d5e1aacaf40a Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Mon, 11 Aug 2025 22:07:45 +0200 Subject: [PATCH 4/8] reduce scopes to gke container readonly Signed-off-by: Raghunandan Balachandran --- .../src/providers/GkeEntityProvider.test.ts | 2 +- .../src/providers/GkeEntityProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 dcd7d5c05e..493a303970 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts @@ -306,7 +306,7 @@ describe('GkeEntityProvider', () => { expect(MockedClusterManagerClient).toHaveBeenCalledWith({ credentials: mockCredentials, - scopes: ['https://www.googleapis.com/auth/cloud-platform'], + scopes: ['https://www.googleapis.com/auth/container.readonly'], }); }); diff --git a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts index a60dd717a5..dbaab2654b 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts @@ -90,7 +90,7 @@ export class GkeEntityProvider implements EntityProvider { const credentialsObject = JSON.parse(credentials); clusterManagerClient = 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 c8b7a221426bef0f6e512093be20ca242909fd9d Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Tue, 12 Aug 2025 09:55:24 +0200 Subject: [PATCH 5/8] remove explicit scopes Signed-off-by: Raghunandan Balachandran --- .../src/providers/GkeEntityProvider.test.ts | 1 - .../src/providers/GkeEntityProvider.ts | 1 - 2 files changed, 2 deletions(-) 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 493a303970..a7d0d7d066 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts @@ -306,7 +306,6 @@ describe('GkeEntityProvider', () => { expect(MockedClusterManagerClient).toHaveBeenCalledWith({ credentials: mockCredentials, - scopes: ['https://www.googleapis.com/auth/container.readonly'], }); }); diff --git a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts index dbaab2654b..bde0c7fc12 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.ts @@ -90,7 +90,6 @@ export class GkeEntityProvider implements EntityProvider { const credentialsObject = JSON.parse(credentials); clusterManagerClient = new container.v1.ClusterManagerClient({ credentials: credentialsObject, - scopes: ['https://www.googleapis.com/auth/container.readonly'], }); } catch (error) { throw new Error( From 4cdcc3707e6cce923518f03020cba8996f980b63 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Tue, 12 Aug 2025 16:48:13 +0200 Subject: [PATCH 6/8] update changeset Signed-off-by: Raghunandan Balachandran --- .changeset/fluffy-bananas-shake.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/fluffy-bananas-shake.md b/.changeset/fluffy-bananas-shake.md index b866cd024c..9b8e878fa4 100644 --- a/.changeset/fluffy-bananas-shake.md +++ b/.changeset/fluffy-bananas-shake.md @@ -2,4 +2,5 @@ '@backstage/plugin-catalog-backend-module-gcp': patch --- -Added support for optional `authProvider` and `owner` configuration parameters for GkeEntityProvider +Added support for Google Service account credentials config used in GkeEntityProvider. +Added support for additional metadata `authProvider` and `owner` to be set for the gke cluster entities. From 388d96bd9166e9870028f34e3f8a44af1c1b1b24 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Tue, 12 Aug 2025 18:58:19 +0200 Subject: [PATCH 7/8] add tests for authProvider and owner metadata Signed-off-by: Raghunandan Balachandran --- .../src/providers/GkeEntityProvider.test.ts | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) 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 a7d0d7d066..90b0f81352 100644 --- a/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gcp/src/providers/GkeEntityProvider.test.ts @@ -244,6 +244,227 @@ describe('GkeEntityProvider', () => { expect(connectionMock.applyMutation).toMatchSnapshot(); }); + describe('authProvider and owner configuration', () => { + it('should use default values when authProvider and owner not configured', async () => { + const defaultGkeEntityProvider = GkeEntityProvider.fromConfigWithClient({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/parent1/locations/-'], + schedule: { + frequency: { minutes: 3 }, + timeout: { minutes: 3 }, + }, + }, + }, + }, + }, + }), + scheduler: schedulerMock, + clusterManagerClient: clusterManagerClientMock as any, + }); + + clusterManagerClientMock.listClusters.mockImplementation(() => [ + { + clusters: [ + { + name: 'default-cluster', + endpoint: '127.0.0.1', + location: 'us-central1', + selfLink: 'https://127.0.0.1/default-link', + masterAuth: { + clusterCaCertificate: 'defaultcert', + }, + }, + ], + }, + ]); + + await defaultGkeEntityProvider.connect(connectionMock); + await defaultGkeEntityProvider.refresh(); + + expect(connectionMock.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'default-cluster', + annotations: expect.objectContaining({ + 'kubernetes.io/auth-provider': 'google', // Default value + }), + }), + spec: expect.objectContaining({ + owner: 'unknown', // Default value + type: 'kubernetes-cluster', + }), + }), + locationKey: 'gcp-gke:us-central1', + }, + ], + }); + }); + + it('should use both custom authProvider and owner when configured', async () => { + const fullyCustomGkeEntityProvider = + GkeEntityProvider.fromConfigWithClient({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/parent1/locations/-'], + authProvider: 'aws', + owner: 'devops-team', + schedule: { + frequency: { minutes: 3 }, + timeout: { minutes: 3 }, + }, + }, + }, + }, + }, + }), + scheduler: schedulerMock, + clusterManagerClient: clusterManagerClientMock as any, + }); + + clusterManagerClientMock.listClusters.mockImplementation(() => [ + { + clusters: [ + { + name: 'aws-auth-cluster', + endpoint: '127.0.0.1', + location: 'asia-southeast1', + selfLink: 'https://127.0.0.1/aws-link', + masterAuth: { + clusterCaCertificate: 'awscert', + }, + }, + ], + }, + ]); + + await fullyCustomGkeEntityProvider.connect(connectionMock); + await fullyCustomGkeEntityProvider.refresh(); + + expect(connectionMock.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'aws-auth-cluster', + annotations: expect.objectContaining({ + 'kubernetes.io/auth-provider': 'aws', + }), + }), + spec: expect.objectContaining({ + owner: 'devops-team', + type: 'kubernetes-cluster', + }), + }), + locationKey: 'gcp-gke:asia-southeast1', + }, + ], + }); + }); + + it('should apply authProvider and owner to multiple clusters', async () => { + const multiClusterGkeEntityProvider = + GkeEntityProvider.fromConfigWithClient({ + logger: logger as any, + config: new ConfigReader({ + catalog: { + providers: { + gcp: { + gke: { + parents: ['projects/parent1/locations/-'], + authProvider: 'oidc', + owner: 'sre-team', + schedule: { + frequency: { minutes: 3 }, + timeout: { minutes: 3 }, + }, + }, + }, + }, + }, + }), + scheduler: schedulerMock, + clusterManagerClient: clusterManagerClientMock as any, + }); + + clusterManagerClientMock.listClusters.mockImplementation(() => [ + { + clusters: [ + { + name: 'cluster-1', + endpoint: '127.0.0.1', + location: 'us-central1', + selfLink: 'https://127.0.0.1/cluster1-link', + masterAuth: { + clusterCaCertificate: 'cert1', + }, + }, + { + name: 'cluster-2', + endpoint: '127.0.0.2', + location: 'us-east1', + selfLink: 'https://127.0.0.2/cluster2-link', + masterAuth: { + clusterCaCertificate: 'cert2', + }, + }, + ], + }, + ]); + + await multiClusterGkeEntityProvider.connect(connectionMock); + await multiClusterGkeEntityProvider.refresh(); + + expect(connectionMock.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'cluster-1', + annotations: expect.objectContaining({ + 'kubernetes.io/auth-provider': 'oidc', + }), + }), + spec: expect.objectContaining({ + owner: 'sre-team', + type: 'kubernetes-cluster', + }), + }), + locationKey: 'gcp-gke:us-central1', + }, + { + entity: expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'cluster-2', + annotations: expect.objectContaining({ + 'kubernetes.io/auth-provider': 'oidc', + }), + }), + spec: expect.objectContaining({ + owner: 'sre-team', + type: 'kubernetes-cluster', + }), + }), + locationKey: 'gcp-gke:us-east1', + }, + ], + }); + }); + }); + it('should log GKE API errors', async () => { clusterManagerClientMock.listClusters.mockRejectedValue( new Error('some-error'), From 5e0e4a3128b4d6992d0132a541de2dc30c00ec91 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 15 Aug 2025 00:13:23 +0200 Subject: [PATCH 8/8] Apply suggestions from code review Signed-off-by: Patrik Oldsberg --- .changeset/fluffy-bananas-shake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fluffy-bananas-shake.md b/.changeset/fluffy-bananas-shake.md index 9b8e878fa4..f7723ded60 100644 --- a/.changeset/fluffy-bananas-shake.md +++ b/.changeset/fluffy-bananas-shake.md @@ -3,4 +3,4 @@ --- Added support for Google Service account credentials config used in GkeEntityProvider. -Added support for additional metadata `authProvider` and `owner` to be set for the gke cluster entities. +Added support for additional metadata `authProvider` and `owner` to be set for the GKE cluster entities.