Store GKE dashboard parameters in catalog

Signed-off-by: Tomasz Szuba <tszuba@box.com>
This commit is contained in:
Tomasz Szuba
2023-10-02 19:09:43 +02:00
parent b3be214807
commit 62180df4ee
12 changed files with 214 additions and 127 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-gcp': patch
---
Allow integration with kubernetes dashboard
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes-common': patch
---
Allow storing dashboard parameters for kubernetes in catalog
@@ -48,6 +48,7 @@
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-kubernetes-common": "workspace:^",
@@ -16,11 +16,6 @@
import { GkeEntityProvider } from './GkeEntityProvider';
import { TaskRunner } from '@backstage/backend-tasks';
import {
ANNOTATION_KUBERNETES_API_SERVER,
ANNOTATION_KUBERNETES_API_SERVER_CA,
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
} from '@backstage/plugin-kubernetes-common';
import * as container from '@google-cloud/container';
import { ConfigReader } from '@backstage/config';
@@ -55,7 +50,10 @@ describe('GkeEntityProvider', () => {
providers: {
gcp: {
gke: {
parents: ['parent1', 'parent2'],
parents: [
'projects/parent1/locations/-',
'projects/parent2/locations/some-other-location',
],
schedule: {
frequency: {
minutes: 3,
@@ -77,7 +75,7 @@ describe('GkeEntityProvider', () => {
it('should return clusters as Resources', async () => {
clusterManagerClientMock.listClusters.mockImplementation(req => {
if (req.parent === 'parent1') {
if (req.parent === 'projects/parent1/locations/-') {
return [
{
clusters: [
@@ -93,7 +91,9 @@ describe('GkeEntityProvider', () => {
],
},
];
} else if (req.parent === 'parent2') {
} else if (
req.parent === 'projects/parent2/locations/some-other-location'
) {
return [
{
clusters: [
@@ -114,58 +114,7 @@ describe('GkeEntityProvider', () => {
throw new Error(`unexpected parent ${req.parent}`);
});
await gkeEntityProvider.refresh();
expect(connectionMock.applyMutation).toHaveBeenCalledWith({
type: 'full',
entities: [
{
locationKey: 'gcp-gke:some-location',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Resource',
metadata: {
annotations: {
[ANNOTATION_KUBERNETES_API_SERVER]: 'https://127.0.0.1',
[ANNOTATION_KUBERNETES_API_SERVER_CA]: 'abcdefg',
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google',
'backstage.io/managed-by-location': 'gcp-gke:some-location',
'backstage.io/managed-by-origin-location':
'gcp-gke:some-location',
},
name: 'some-cluster',
namespace: 'default',
},
spec: {
type: 'kubernetes-cluster',
owner: 'unknown',
},
},
},
{
locationKey: 'gcp-gke:some-other-location',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Resource',
metadata: {
annotations: {
[ANNOTATION_KUBERNETES_API_SERVER]: 'https://127.0.0.1',
[ANNOTATION_KUBERNETES_API_SERVER_CA]: '',
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google',
'backstage.io/managed-by-location':
'gcp-gke:some-other-location',
'backstage.io/managed-by-origin-location':
'gcp-gke:some-other-location',
},
name: 'some-other-cluster',
namespace: 'default',
},
spec: {
type: 'kubernetes-cluster',
owner: 'unknown',
},
},
},
],
});
expect(connectionMock.applyMutation).toMatchSnapshot();
});
const ignoredPartialClustersTests: [
@@ -223,7 +172,7 @@ describe('GkeEntityProvider', () => {
'ignore cluster - %s',
async (_name, ignoredCluster) => {
clusterManagerClientMock.listClusters.mockImplementation(req => {
if (req.parent === 'parent1') {
if (req.parent === 'projects/parent1/locations/-') {
return [ignoredCluster];
}
return [
@@ -29,9 +29,15 @@ import {
ANNOTATION_KUBERNETES_API_SERVER,
ANNOTATION_KUBERNETES_API_SERVER_CA,
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
ANNOTATION_KUBERNETES_DASHBOARD_APP,
ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS,
} from '@backstage/plugin-kubernetes-common';
import { Config } from '@backstage/config';
import { SchedulerService } from '@backstage/backend-plugin-api';
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
} from '@backstage/catalog-model';
/**
* Catalog provider to ingest GKE clusters
@@ -120,10 +126,16 @@ export class GkeEntityProvider implements EntityProvider {
private clusterToResource(
cluster: container.protos.google.container.v1.ICluster,
project: string,
): DeferredEntity | undefined {
const location = `${this.getProviderName()}:${cluster.location}`;
if (!cluster.name || !cluster.selfLink || !location || !cluster.endpoint) {
if (
!cluster.name ||
!cluster.selfLink ||
!cluster.endpoint ||
!cluster.location
) {
this.logger.warn(
`ignoring partial cluster, one of name=${cluster.name}, endpoint=${cluster.endpoint}, selfLink=${cluster.selfLink} or location=${cluster.location} is missing`,
);
@@ -142,8 +154,14 @@ export class GkeEntityProvider implements EntityProvider {
[ANNOTATION_KUBERNETES_API_SERVER_CA]:
cluster.masterAuth?.clusterCaCertificate || '',
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google',
'backstage.io/managed-by-location': location,
'backstage.io/managed-by-origin-location': location,
[ANNOTATION_KUBERNETES_DASHBOARD_APP]: 'gke',
[ANNOTATION_LOCATION]: location,
[ANNOTATION_ORIGIN_LOCATION]: location,
[ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS]: JSON.stringify({
projectId: project,
region: cluster.location,
clusterName: cluster.name,
}),
},
name: cluster.name,
namespace: 'default',
@@ -172,18 +190,22 @@ export class GkeEntityProvider implements EntityProvider {
};
}
private async getClusters(): Promise<
container.protos.google.container.v1.ICluster[]
> {
private async getClusters(): Promise<DeferredEntity[]> {
const clusters = await Promise.all(
this.gkeParents.map(async parent => {
const project = parent.split('/')[1];
const request = {
parent: parent,
};
const [response] = await this.clusterManagerClient.listClusters(
request,
);
return response.clusters?.filter(this.filterOutUndefinedCluster) ?? [];
return (
response.clusters
?.filter(this.filterOutUndefinedCluster)
.map(c => this.clusterToResource(c, project))
.filter(this.filterOutUndefinedDeferredEntity) ?? []
);
}),
);
return clusters.flat();
@@ -196,18 +218,14 @@ export class GkeEntityProvider implements EntityProvider {
this.logger.info('Discovering GKE clusters');
let clusters: container.protos.google.container.v1.ICluster[];
let resources: DeferredEntity[];
try {
clusters = await this.getClusters();
resources = await this.getClusters();
} catch (e) {
this.logger.error('error fetching GKE clusters', e);
return;
}
const resources =
clusters
.map(c => this.clusterToResource(c))
.filter(this.filterOutUndefinedDeferredEntity) ?? [];
this.logger.info(
`Ingesting GKE clusters [${resources
@@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GkeEntityProvider should return clusters as Resources 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": "google",
"kubernetes.io/dashboard-app": "gke",
"kubernetes.io/dashboard-parameters": "{"projectId":"parent1","region":"some-location","clusterName":"some-cluster"}",
},
"name": "some-cluster",
"namespace": "default",
},
"spec": {
"owner": "unknown",
"type": "kubernetes-cluster",
},
},
"locationKey": "gcp-gke:some-location",
},
{
"entity": {
"apiVersion": "backstage.io/v1alpha1",
"kind": "Resource",
"metadata": {
"annotations": {
"backstage.io/managed-by-location": "gcp-gke:some-other-location",
"backstage.io/managed-by-origin-location": "gcp-gke:some-other-location",
"kubernetes.io/api-server": "https://127.0.0.1",
"kubernetes.io/api-server-certificate-authority": "",
"kubernetes.io/auth-provider": "google",
"kubernetes.io/dashboard-app": "gke",
"kubernetes.io/dashboard-parameters": "{"projectId":"parent2","region":"some-other-location","clusterName":"some-other-cluster"}",
},
"name": "some-other-cluster",
"namespace": "default",
},
"spec": {
"owner": "unknown",
"type": "kubernetes-cluster",
},
},
"locationKey": "gcp-gke:some-other-location",
},
],
"type": "full",
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;
@@ -23,7 +23,6 @@ import {
} from '@backstage/plugin-kubernetes-common';
import { CatalogClusterLocator } from './CatalogClusterLocator';
import { CatalogApi } from '@backstage/catalog-client';
import { ClusterDetails } from '../types/types';
const mockCatalogApi = {
getEntityByRef: jest.fn(),
@@ -93,25 +92,7 @@ describe('CatalogClusterLocator', () => {
const result = await clusterSupplier.getClusters();
expect(result).toHaveLength(2);
expect(result[0]).toStrictEqual<ClusterDetails>({
name: 'owned',
url: 'https://apiserver.com',
caData: 'caData',
authMetadata: {
'kubernetes.io/api-server': 'https://apiserver.com',
'kubernetes.io/api-server-certificate-authority': 'caData',
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google',
'kubernetes.io/skip-metrics-lookup': 'true',
'kubernetes.io/skip-tls-verify': 'true',
'kubernetes.io/dashboard-url': 'my-url',
'kubernetes.io/dashboard-app': 'my-app',
},
skipMetricsLookup: true,
skipTLSVerify: true,
dashboardUrl: 'my-url',
dashboardApp: 'my-app',
});
expect(result[0]).toMatchSnapshot();
});
it('returns the aws cluster details provided by annotations', async () => {
@@ -120,24 +101,6 @@ describe('CatalogClusterLocator', () => {
const result = await clusterSupplier.getClusters();
expect(result).toHaveLength(2);
expect(result[1]).toStrictEqual<ClusterDetails>({
name: 'owned',
url: 'https://apiserver.com',
caData: 'caData',
authMetadata: {
'kubernetes.io/api-server': 'https://apiserver.com',
'kubernetes.io/api-server-certificate-authority': 'caData',
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role',
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google',
'kubernetes.io/dashboard-url': 'my-url',
'kubernetes.io/dashboard-app': 'my-app',
},
skipMetricsLookup: false,
skipTLSVerify: false,
dashboardUrl: 'my-url',
dashboardApp: 'my-app',
});
expect(result[1]).toMatchSnapshot();
});
});
@@ -24,7 +24,13 @@ import {
ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY,
ANNOTATION_KUBERNETES_DASHBOARD_URL,
ANNOTATION_KUBERNETES_DASHBOARD_APP,
ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS,
} from '@backstage/plugin-kubernetes-common';
import { JsonObject } from '@backstage/types';
function isObject(obj: unknown): obj is JsonObject {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
export class CatalogClusterLocator implements KubernetesClustersSupplier {
private catalogClient: CatalogApi;
@@ -54,27 +60,38 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier {
filter: [filter],
});
return clusters.items.map(entity => {
const annotations = entity.metadata.annotations!;
const clusterDetails: ClusterDetails = {
name: entity.metadata.name,
url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!,
authMetadata: entity.metadata.annotations!,
caData:
entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!,
url: annotations[ANNOTATION_KUBERNETES_API_SERVER],
authMetadata: annotations,
caData: annotations[ANNOTATION_KUBERNETES_API_SERVER_CA],
skipMetricsLookup:
entity.metadata.annotations![
ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP
]! === 'true',
annotations[ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP] === 'true',
skipTLSVerify:
entity.metadata.annotations![
ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY
]! === 'true',
dashboardUrl:
entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_URL]!,
dashboardApp:
entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_APP]!,
annotations[ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY] === 'true',
dashboardUrl: annotations[ANNOTATION_KUBERNETES_DASHBOARD_URL],
dashboardApp: annotations[ANNOTATION_KUBERNETES_DASHBOARD_APP],
dashboardParameters: this.getDashboardParameters(annotations),
};
return clusterDetails;
});
}
private getDashboardParameters(
annotations: Record<string, string>,
): JsonObject | undefined {
const dashboardParamsString =
annotations[ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS];
if (dashboardParamsString) {
try {
const dashboardParams = JSON.parse(dashboardParamsString);
return isObject(dashboardParams) ? dashboardParams : undefined;
} catch {
return undefined;
}
}
return undefined;
}
}
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CatalogClusterLocator returns the aws cluster details provided by annotations 1`] = `
{
"authMetadata": {
"kubernetes.io/api-server": "https://apiserver.com",
"kubernetes.io/api-server-certificate-authority": "caData",
"kubernetes.io/auth-provider": "aws",
"kubernetes.io/aws-assume-role": "my-role",
"kubernetes.io/aws-external-id": "my-id",
"kubernetes.io/dashboard-app": "my-app",
"kubernetes.io/dashboard-url": "my-url",
"kubernetes.io/oidc-token-provider": "google",
},
"caData": "caData",
"dashboardApp": "my-app",
"dashboardParameters": undefined,
"dashboardUrl": "my-url",
"name": "owned",
"skipMetricsLookup": false,
"skipTLSVerify": false,
"url": "https://apiserver.com",
}
`;
exports[`CatalogClusterLocator returns the cluster details provided by annotations 1`] = `
{
"authMetadata": {
"kubernetes.io/api-server": "https://apiserver.com",
"kubernetes.io/api-server-certificate-authority": "caData",
"kubernetes.io/auth-provider": "oidc",
"kubernetes.io/dashboard-app": "my-app",
"kubernetes.io/dashboard-url": "my-url",
"kubernetes.io/oidc-token-provider": "google",
"kubernetes.io/skip-metrics-lookup": "true",
"kubernetes.io/skip-tls-verify": "true",
},
"caData": "caData",
"dashboardApp": "my-app",
"dashboardParameters": undefined,
"dashboardUrl": "my-url",
"name": "owned",
"skipMetricsLookup": true,
"skipTLSVerify": true,
"url": "https://apiserver.com",
}
`;
+4
View File
@@ -46,6 +46,10 @@ export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID =
export const ANNOTATION_KUBERNETES_DASHBOARD_APP =
'kubernetes.io/dashboard-app';
// @public
export const ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS =
'kubernetes.io/dashboard-parameters';
// @public
export const ANNOTATION_KUBERNETES_DASHBOARD_URL =
'kubernetes.io/dashboard-url';
@@ -77,6 +77,13 @@ export const ANNOTATION_KUBERNETES_DASHBOARD_URL =
export const ANNOTATION_KUBERNETES_DASHBOARD_APP =
'kubernetes.io/dashboard-app';
/**
* Annotation for specifying the dashboard app parameters for a Kubernetes cluster.
*
* @public
*/
export const ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS =
'kubernetes.io/dashboard-parameters';
/**
* Annotation for specifying the assume role use to authenticate with AWS.
*
+1
View File
@@ -5432,6 +5432,7 @@ __metadata:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"