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:
@@ -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/..."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user