Adds support for using google service account credentials in the catalog provider gcp module

Signed-off-by: Raghunandan Balachandran <raghunandan@spotify.com>
This commit is contained in:
Raghunandan Balachandran
2025-08-11 11:13:14 +02:00
parent cdb146cbd0
commit 9f36f8f01f
4 changed files with 227 additions and 1 deletions
@@ -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/..."
}
```
+6
View File
@@ -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;
};
};
};
@@ -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();
});
});
});
@@ -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,
});
}