From ec98274a7ef5fbca1baaec20606433d9dee9b1e6 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 14 Jul 2021 12:50:24 +0100 Subject: [PATCH] Creating a Cluster details object per k8s provider Signed-off-by: Nicolas Arnold --- plugins/kubernetes-backend/api-report.md | 17 ++++++- .../ConfigClusterLocator.test.ts | 50 ++++++++++++++++--- .../cluster-locator/ConfigClusterLocator.ts | 24 +++++++-- .../src/cluster-locator/GkeClusterLocator.ts | 4 +- .../src/cluster-locator/index.test.ts | 3 -- .../AwsIamKubernetesAuthTranslator.ts | 23 ++++----- .../GoogleKubernetesAuthTranslator.ts | 11 ++-- .../KubernetesAuthTranslatorGenerator.test.ts | 15 +++--- .../ServiceAccountKubernetesAuthTranslator.ts | 9 ++-- .../src/service/KubernetesFanOutHandler.ts | 12 ++--- .../src/service/KubernetesFetcher.ts | 38 +++++++------- .../kubernetes-backend/src/service/router.ts | 5 +- plugins/kubernetes-backend/src/types/types.ts | 10 +++- 13 files changed, 147 insertions(+), 74 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index b7ece06ed5..0de29bd2f5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -13,9 +13,13 @@ import { Logger as Logger_2 } from 'winston'; // Warning: (ae-missing-release-tag) "ClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ClusterDetails { +export interface AWSClusterDetails extends ClusterDetails { // (undocumented) assumeRole?: string; +} + +// @public (undocumented) +export interface ClusterDetails { // (undocumented) authProvider: string; // (undocumented) @@ -57,6 +61,9 @@ export interface FetchResponseWrapper { // Warning: (ae-missing-release-tag) "KubernetesClustersSupplier" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // +// @public (undocumented) +export interface GKEClusterDetails extends ClusterDetails {} + // @public (undocumented) export interface KubernetesClustersSupplier { // (undocumented) @@ -109,7 +116,10 @@ export const makeRouter: ( // @public (undocumented) export interface ObjectFetchParams { // (undocumented) - clusterDetails: ClusterDetails; + clusterDetails: + | AWSClusterDetails + | GKEClusterDetails + | ServiceAccountClusterDetails; // (undocumented) customResources: CustomResource[]; // (undocumented) @@ -134,6 +144,9 @@ export interface RouterOptions { // Warning: (ae-missing-release-tag) "ServiceLocatorMethod" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // +// @public (undocumented) +export interface ServiceAccountClusterDetails extends ClusterDetails {} + // @public (undocumented) export type ServiceLocatorMethod = 'multiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index eb5e6ff605..1642ca5580 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -35,7 +35,6 @@ describe('ConfigClusterLocator', () => { const config: Config = new ConfigReader({ clusters: [ { - assumeRole: 'SomeRole', name: 'cluster1', url: 'http://localhost:8080', authProvider: 'serviceAccount', @@ -49,7 +48,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { - assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: undefined, url: 'http://localhost:8080', @@ -63,7 +61,6 @@ describe('ConfigClusterLocator', () => { const config: Config = new ConfigReader({ clusters: [ { - assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -71,7 +68,6 @@ describe('ConfigClusterLocator', () => { skipTLSVerify: false, }, { - assumeRole: undefined, name: 'cluster2', url: 'http://localhost:8081', authProvider: 'google', @@ -86,7 +82,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { - assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -94,7 +89,6 @@ describe('ConfigClusterLocator', () => { skipTLSVerify: false, }, { - assumeRole: undefined, name: 'cluster2', serviceAccountToken: undefined, url: 'http://localhost:8081', @@ -103,4 +97,48 @@ describe('ConfigClusterLocator', () => { }, ]); }); + + it('one aws cluster with assumeRole and one without', async () => { + const config: Config = new ConfigReader({ + clusters: [ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'aws', + skipTLSVerify: false, + }, + { + assumeRole: 'SomeRole', + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'aws', + skipTLSVerify: true, + }, + ], + }); + + const sut = ConfigClusterLocator.fromConfig(config); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + assumeRole: undefined, + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'aws', + skipTLSVerify: false, + }, + { + assumeRole: 'SomeRole', + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + authProvider: 'aws', + skipTLSVerify: true, + }, + ]); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index d2fdbe4211..2899445b2a 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -29,14 +29,32 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { // is required if authProvider is serviceAccount return new ConfigClusterLocator( config.getConfigArray('clusters').map(c => { - return { + const authProvider = c.getString('authProvider'); + const clusterDetails = { name: c.getString('name'), url: c.getString('url'), serviceAccountToken: c.getOptionalString('serviceAccountToken'), skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false, - authProvider: c.getString('authProvider'), - assumeRole: c.getOptionalString('assumeRole'), + authProvider: authProvider, }; + + switch (authProvider) { + case 'google': { + return clusterDetails; + } + case 'aws': { + const assumeRole = c.getOptionalString('assumeRole'); + return { assumeRole, ...clusterDetails }; + } + case 'serviceAccount': { + return clusterDetails; + } + default: { + throw new Error( + `authProvider "${authProvider}" has no config associated with it`, + ); + } + } }), ); } diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 92212d0224..3cc6c216ae 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import * as container from '@google-cloud/container'; -import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { GKEClusterDetails, KubernetesClustersSupplier } from '../types/types'; type GkeClusterLocatorOptions = { projectId: string; @@ -49,7 +49,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { ); } - async getClusters(): Promise { + async getClusters(): Promise { const { projectId, region, skipTLSVerify } = this.options; const request = { parent: `projects/${projectId}/locations/${region}`, diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 9725d294a9..95be99a8a5 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -27,7 +27,6 @@ describe('getCombinedClusterDetails', () => { type: 'config', clusters: [ { - assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -50,7 +49,6 @@ describe('getCombinedClusterDetails', () => { expect(result).toStrictEqual([ { - assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -58,7 +56,6 @@ describe('getCombinedClusterDetails', () => { skipTLSVerify: false, }, { - assumeRole: undefined, name: 'cluster2', serviceAccountToken: undefined, url: 'http://localhost:8081', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 15dfa9e14e..e9004818a2 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -15,19 +15,17 @@ */ import AWS from 'aws-sdk'; import { sign } from 'aws4'; -import { ClusterDetails } from '../types/types'; +import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; const base64 = (str: string) => Buffer.from(str.toString(), 'binary').toString('base64'); const prepend = (prep: string) => (str: string) => prep + str; -const replace = - (search: string | RegExp, substitution: string) => (str: string) => - str.replace(search, substitution); -const pipe = - (fns: ReadonlyArray) => - (thing: string): string => - fns.reduce((val, fn) => fn(val), thing); +const replace = (search: string | RegExp, substitution: string) => ( + str: string, +) => str.replace(search, substitution); +const pipe = (fns: ReadonlyArray) => (thing: string): string => + fns.reduce((val, fn) => fn(val), thing); const removePadding = replace(/=+$/, ''); const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]); @@ -38,8 +36,7 @@ type SigningCreds = { }; export class AwsIamKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ + implements KubernetesAuthTranslator { validCredentials(creds: SigningCreds): boolean { if (!creds.accessKeyId || !creds.secretAccessKey || !creds.sessionToken) { return false; @@ -116,9 +113,9 @@ export class AwsIamKubernetesAuthTranslator } async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( + clusterDetails: AWSClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: AWSClusterDetails = Object.assign( {}, clusterDetails, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index 62ffd823cf..eacba4d3e1 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -15,17 +15,16 @@ */ import { KubernetesAuthTranslator } from './types'; -import { ClusterDetails } from '../types/types'; +import { GKEClusterDetails } from '../types/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; export class GoogleKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ + implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, + clusterDetails: GKEClusterDetails, requestBody: KubernetesRequestBody, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( + ): Promise { + const clusterDetailsWithAuthToken: GKEClusterDetails = Object.assign( {}, clusterDetails, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts index 33592b1c41..2d900b0bd1 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -24,20 +24,23 @@ describe('getKubernetesAuthTranslatorInstance', () => { const sut = KubernetesAuthTranslatorGenerator; it('can return an auth translator for google auth', () => { - const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('google'); + const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance( + 'google', + ); expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for aws auth', () => { - const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('aws'); + const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance( + 'aws', + ); expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for serviceAccount auth', () => { - const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('serviceAccount'); + const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance( + 'serviceAccount', + ); expect( authTranslator instanceof ServiceAccountKubernetesAuthTranslator, ).toBe(true); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts index 098ffe3330..1c3add3c0d 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts @@ -15,19 +15,18 @@ */ import { KubernetesAuthTranslator } from './types'; -import { ClusterDetails } from '../types/types'; +import { ServiceAccountClusterDetails } from '../types/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; export class ServiceAccountKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ + implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, + clusterDetails: ServiceAccountClusterDetails, // To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read. // @ts-ignore-start requestBody: KubernetesRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars // @ts-ignore-end - ): Promise { + ): Promise { return clusterDetails; } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index e088fddd41..3039560aad 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -63,15 +63,15 @@ export class KubernetesFanOutHandler { 'backstage.io/kubernetes-id' ] || requestBody.entity?.metadata?.name; - const clusterDetails: ClusterDetails[] = - await this.serviceLocator.getClustersByServiceId(entityName); + const clusterDetails: ClusterDetails[] = await this.serviceLocator.getClustersByServiceId( + entityName, + ); // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them const promises: Promise[] = clusterDetails.map(cd => { - const kubernetesAuthTranslator: KubernetesAuthTranslator = - KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( - cd.authProvider, - ); + const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( + cd.authProvider, + ); return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( cd, requestBody, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index e17b47e17a..76c3b0aac0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -167,9 +167,10 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { labelSelector, ).then(r => ({ type: type, resources: r })); case 'services': - return this.fetchServicesForService(clusterDetails, labelSelector).then( - r => ({ type: type, resources: r }), - ); + return this.fetchServicesForService( + clusterDetails, + labelSelector, + ).then(r => ({ type: type, resources: r })); case 'horizontalpodautoscalers': return this.fetchHorizontalPodAutoscalersForService( clusterDetails, @@ -191,8 +192,9 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { customResource: CustomResource, labelSelector: string, ): Promise { - const customObjects = - this.kubernetesClientProvider.getCustomObjectsClient(clusterDetails); + const customObjects = this.kubernetesClientProvider.getCustomObjectsClient( + clusterDetails, + ); return customObjects .listClusterCustomObject( @@ -215,20 +217,18 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { client: Clients, ) => Promise<{ body: { items: Array }; response: http.IncomingMessage }>, ): Promise> { - const core = - this.kubernetesClientProvider.getCoreClientByClusterDetails( - clusterDetails, - ); - const apps = - this.kubernetesClientProvider.getAppsClientByClusterDetails( - clusterDetails, - ); - const autoscaling = - this.kubernetesClientProvider.getAutoscalingClientByClusterDetails( - clusterDetails, - ); - const networkingBeta1 = - this.kubernetesClientProvider.getNetworkingBeta1Client(clusterDetails); + const core = this.kubernetesClientProvider.getCoreClientByClusterDetails( + clusterDetails, + ); + const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails( + clusterDetails, + ); + const autoscaling = this.kubernetesClientProvider.getAutoscalingClientByClusterDetails( + clusterDetails, + ); + const networkingBeta1 = this.kubernetesClientProvider.getNetworkingBeta1Client( + clusterDetails, + ); this.logger.debug(`calling cluster=${clusterDetails.name}`); return fn({ core, apps, autoscaling, networkingBeta1 }).then(({ body }) => { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index beebd6fa65..984b3f14a0 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -70,8 +70,9 @@ export const makeRouter = ( const serviceId = req.params.serviceId; const requestBody: KubernetesRequestBody = req.body; try { - const response = - await kubernetesFanOutHandler.getKubernetesObjectsByEntity(requestBody); + const response = await kubernetesFanOutHandler.getKubernetesObjectsByEntity( + requestBody, + ); res.json(response); } catch (e) { logger.error( diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 5150eba8cf..c5f555d588 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -27,7 +27,10 @@ export interface CustomResource { export interface ObjectFetchParams { serviceId: string; - clusterDetails: ClusterDetails; + clusterDetails: + | AWSClusterDetails + | GKEClusterDetails + | ServiceAccountClusterDetails; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; @@ -76,5 +79,10 @@ export interface ClusterDetails { authProvider: string; serviceAccountToken?: string | undefined; skipTLSVerify?: boolean; +} + +export interface GKEClusterDetails extends ClusterDetails {} +export interface ServiceAccountClusterDetails extends ClusterDetails {} +export interface AWSClusterDetails extends ClusterDetails { assumeRole?: string; }