From 6d59d17a7fbb2dc91629c9ed3c380878841966eb Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 14 Jul 2021 10:20:23 +0100 Subject: [PATCH 1/7] Add functionality to the kubernetes plugin that allows users to assume role This change adds a new field to the kubernetes plugin configuration that allows a user to configure an assumed role so that they can access other kubernetes cluster. Signed-off-by: Nicolas Arnold --- plugins/kubernetes-backend/package.json | 4 +- .../ConfigClusterLocator.test.ts | 6 ++ .../cluster-locator/ConfigClusterLocator.ts | 1 + .../src/cluster-locator/index.test.ts | 3 + .../AwsIamKubernetesAuthTranslator.test.ts | 80 ++++++++++++---- .../AwsIamKubernetesAuthTranslator.ts | 91 ++++++++++++++----- .../GoogleKubernetesAuthTranslator.ts | 3 +- .../KubernetesAuthTranslatorGenerator.test.ts | 15 ++- .../ServiceAccountKubernetesAuthTranslator.ts | 3 +- .../src/service/KubernetesFanOutHandler.ts | 12 +-- .../src/service/KubernetesFetcher.ts | 38 ++++---- .../kubernetes-backend/src/service/router.ts | 5 +- plugins/kubernetes-backend/src/types/types.ts | 1 + 13 files changed, 184 insertions(+), 78 deletions(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 6bc7382f54..720321a180 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -55,7 +55,9 @@ "devDependencies": { "@backstage/cli": "^0.7.4", "@types/aws4": "^1.5.1", - "supertest": "^6.1.3" + "supertest": "^6.1.3", + "aws-sdk-mock": "^5.2.1", + "bdd-lazy-var": "^2.6.0" }, "files": [ "dist", diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index ac4a742828..eb5e6ff605 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -35,6 +35,7 @@ describe('ConfigClusterLocator', () => { const config: Config = new ConfigReader({ clusters: [ { + assumeRole: 'SomeRole', name: 'cluster1', url: 'http://localhost:8080', authProvider: 'serviceAccount', @@ -48,6 +49,7 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { + assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: undefined, url: 'http://localhost:8080', @@ -61,6 +63,7 @@ describe('ConfigClusterLocator', () => { const config: Config = new ConfigReader({ clusters: [ { + assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -68,6 +71,7 @@ describe('ConfigClusterLocator', () => { skipTLSVerify: false, }, { + assumeRole: undefined, name: 'cluster2', url: 'http://localhost:8081', authProvider: 'google', @@ -82,6 +86,7 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { + assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -89,6 +94,7 @@ describe('ConfigClusterLocator', () => { skipTLSVerify: false, }, { + assumeRole: undefined, name: 'cluster2', serviceAccountToken: undefined, url: 'http://localhost:8081', diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 268cca0193..d2fdbe4211 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -35,6 +35,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { serviceAccountToken: c.getOptionalString('serviceAccountToken'), skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false, authProvider: c.getString('authProvider'), + assumeRole: c.getOptionalString('assumeRole'), }; }), ); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 95be99a8a5..9725d294a9 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -27,6 +27,7 @@ describe('getCombinedClusterDetails', () => { type: 'config', clusters: [ { + assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -49,6 +50,7 @@ describe('getCombinedClusterDetails', () => { expect(result).toStrictEqual([ { + assumeRole: 'SomeRole', name: 'cluster1', serviceAccountToken: 'token', url: 'http://localhost:8080', @@ -56,6 +58,7 @@ 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.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index 348ad541d5..d5179fc07a 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -14,15 +14,49 @@ * limitations under the License. */ import AWS from 'aws-sdk'; +import AWSMock from 'aws-sdk-mock'; import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; +import { get, def } from 'bdd-lazy-var'; describe('AwsIamKubernetesAuthTranslator tests', () => { + let valid: boolean = true; + let role: any = undefined; + let response: any = { + Credentials: { + AccessKeyId: 'bloop', + SecretAccessKey: 'omg-so-secret', + SessionToken: 'token', + }, + }; + + AWSMock.setSDKInstance(AWS); + beforeEach(() => { jest.resetAllMocks(); }); - it('returns a signed url for aws credentials', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + afterAll(() => { + jest.resetAllMocks(); + }); + + def('subject', () => { + AWSMock.mock('STS', 'assumeRole', (_params: any, callback: Function) => { + callback(null, response); + }); + + const authTranslator = new AwsIamKubernetesAuthTranslator(); + jest + .spyOn(authTranslator, 'validCredentials') + .mockImplementation(() => valid); + return authTranslator.decorateClusterDetailsWithAuth({ + assumeRole: role, + name: 'test-cluster', + url: '', + authProvider: 'aws', + }); + }); + + it('returns a signed url for aws credentials', async () => { // These credentials are not real. // Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html AWS.config.credentials = new AWS.Credentials( @@ -30,24 +64,38 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', ); - const clusterDetails = await authTranslator.decorateClusterDetailsWithAuth({ - name: 'test-cluster', - url: '', - authProvider: 'aws', + const subject = await get('subject'); + expect(subject.serviceAccountToken).toBeDefined(); + }); + + describe('When the role is assumed', () => { + // These credentials are not real. + // Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html + AWS.config.credentials = new AWS.Credentials( + 'AKIAIOSFODNN7EXAMPLE', + 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + ); + role = 'SomeRole'; + + describe('When the role is valid', () => { + it('returns a signed url for aws credentials', async () => { + const subject = await get('subject'); + expect(subject.serviceAccountToken).toBeDefined(); + }); + }); + + describe('When the role is invalid', () => { + it('returns the original AWS credentials', async () => { + response = undefined; + + await expect(get('subject')).rejects.toThrow(/Unable to assume role:/); + }); }); - expect(clusterDetails.serviceAccountToken).toBeDefined(); }); it('throws when unable to get aws credentials', async () => { + valid = false; AWS.config.credentials = undefined; - const authTranslator = new AwsIamKubernetesAuthTranslator(); - const promise = authTranslator.decorateClusterDetailsWithAuth({ - name: 'test-cluster', - url: '', - authProvider: 'aws', - }); - await expect(promise).rejects.toThrow( - 'Could not load credentials from any providers', - ); + await expect(get('subject')).rejects.toThrow('No AWS credentials found'); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 0909926d0d..15dfa9e14e 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import AWS, { Credentials } from 'aws-sdk'; +import AWS from 'aws-sdk'; import { sign } from 'aws4'; import { ClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; @@ -21,31 +21,80 @@ 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('/', '_')]); +type SigningCreds = { + accessKeyId: string | undefined; + secretAccessKey: string | undefined; + sessionToken: string | undefined; +}; + export class AwsIamKubernetesAuthTranslator - implements KubernetesAuthTranslator { - async getBearerToken(clusterName: string): Promise { - const credentials = await new Promise((resolve, reject) => { - AWS.config.getCredentials(err => { + implements KubernetesAuthTranslator +{ + validCredentials(creds: SigningCreds): boolean { + if (!creds.accessKeyId || !creds.secretAccessKey || !creds.sessionToken) { + return false; + } + return true; + } + + async getCredentials(assumeRole: string | undefined): Promise { + return new Promise(async (resolve, reject) => { + await AWS.config.getCredentials(err => { if (err) { + console.error('Unable to load aws config.'); reject(err); - } else { - resolve(AWS.config.credentials); } }); - }); - if (!(credentials instanceof Credentials)) { - throw new Error('no AWS credentials found.'); - } - await credentials.getPromise(); + let creds: SigningCreds = { + accessKeyId: AWS.config.credentials?.accessKeyId, + secretAccessKey: AWS.config.credentials?.secretAccessKey, + sessionToken: AWS.config.credentials?.sessionToken, + }; + + if (!this.validCredentials(creds)) + return reject(Error('No AWS credentials found.')); + if (!assumeRole) return resolve(creds); + + try { + const params = { + RoleArn: assumeRole, + RoleSessionName: 'backstage-login', + }; + const assumedRole = await new AWS.STS().assumeRole(params).promise(); + + if (!assumedRole.Credentials) { + throw new Error(`No credentials returned for role ${assumeRole}`); + } + + creds = { + accessKeyId: assumedRole.Credentials.AccessKeyId, + secretAccessKey: assumedRole.Credentials.SecretAccessKey, + sessionToken: assumedRole.Credentials.SessionToken, + }; + } catch (e) { + console.warn(`There was an error assuming the role: ${e}`); + return reject(Error(`Unable to assume role: ${e}`)); + } + return resolve(creds); + }); + } + async getBearerToken( + clusterName: string, + assumeRole: string | undefined, + ): Promise { + const credentials = await this.getCredentials(assumeRole); + const request = { host: `sts.amazonaws.com`, path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`, @@ -54,11 +103,8 @@ export class AwsIamKubernetesAuthTranslator }, signQuery: true, }; - const signedRequest = sign(request, { - accessKeyId: credentials.accessKeyId, - secretAccessKey: credentials.secretAccessKey, - sessionToken: credentials.sessionToken, - }); + + const signedRequest = sign(request, credentials); return pipe([ (signed: any) => `https://${signed.host}${signed.path}`, @@ -79,6 +125,7 @@ export class AwsIamKubernetesAuthTranslator clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken( clusterDetails.name, + clusterDetails.assumeRole, ); return clusterDetailsWithAuthToken; } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index 02a7f314af..62ffd823cf 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -19,7 +19,8 @@ import { ClusterDetails } from '../types/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; export class GoogleKubernetesAuthTranslator - implements KubernetesAuthTranslator { + implements KubernetesAuthTranslator +{ async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, requestBody: KubernetesRequestBody, 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 2d900b0bd1..33592b1c41 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -24,23 +24,20 @@ 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 c433abf4de..098ffe3330 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts @@ -19,7 +19,8 @@ import { ClusterDetails } from '../types/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; export class ServiceAccountKubernetesAuthTranslator - implements KubernetesAuthTranslator { + implements KubernetesAuthTranslator +{ async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, // To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read. diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 3039560aad..e088fddd41 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 76c3b0aac0..e17b47e17a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -167,10 +167,9 @@ 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, @@ -192,9 +191,8 @@ 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( @@ -217,18 +215,20 @@ 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 984b3f14a0..beebd6fa65 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -70,9 +70,8 @@ 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 c6e1668a5a..5150eba8cf 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -76,4 +76,5 @@ export interface ClusterDetails { authProvider: string; serviceAccountToken?: string | undefined; skipTLSVerify?: boolean; + assumeRole?: string; } From 5bd57f8f5dfced87a104f2f104f90b6732e89de1 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 14 Jul 2021 10:27:42 +0100 Subject: [PATCH 2/7] Adding changesets Signed-off-by: Nicolas Arnold --- .changeset/blue-feet-poke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-feet-poke.md diff --git a/.changeset/blue-feet-poke.md b/.changeset/blue-feet-poke.md new file mode 100644 index 0000000000..3be886c687 --- /dev/null +++ b/.changeset/blue-feet-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +Support assume role on kubernetes api configuration for AWS. From f69d4085834203b2c78fb951fdeb8ccb0ffbaac2 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 14 Jul 2021 10:42:56 +0100 Subject: [PATCH 3/7] Add API.md for plugin Signed-off-by: Nicolas Arnold --- plugins/kubernetes-backend/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 1592f20880..b7ece06ed5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -14,6 +14,8 @@ import { Logger as Logger_2 } from 'winston'; // // @public (undocumented) export interface ClusterDetails { + // (undocumented) + assumeRole?: string; // (undocumented) authProvider: string; // (undocumented) From ec98274a7ef5fbca1baaec20606433d9dee9b1e6 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 14 Jul 2021 12:50:24 +0100 Subject: [PATCH 4/7] 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; } From 76fbdbc322e54825b623894928ea04b510351867 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Mon, 19 Jul 2021 17:47:53 +0100 Subject: [PATCH 5/7] Modifying changesets and explicitly returning a promise for the AWS Creds This changes modifies the changeset to only include a patch (as per comments above). I have also changed the flow of retrieving the AWS creds. Previously, they were being returned after they were "resolved". Now we await and resolve a promise to guarantee that the credentials have been loaded. Signed-off-by: Nicolas Arnold --- .changeset/blue-feet-poke.md | 2 +- plugins/kubernetes-backend/api-report.md | 12 +++-- .../AwsIamKubernetesAuthTranslator.test.ts | 46 +++++++++++++------ .../AwsIamKubernetesAuthTranslator.ts | 38 ++++++++++----- 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/.changeset/blue-feet-poke.md b/.changeset/blue-feet-poke.md index 3be886c687..1a9e32a11a 100644 --- a/.changeset/blue-feet-poke.md +++ b/.changeset/blue-feet-poke.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-kubernetes-backend': minor +'@backstage/plugin-kubernetes-backend': patch --- Support assume role on kubernetes api configuration for AWS. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 0de29bd2f5..9f35fda79f 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -10,7 +10,7 @@ import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; 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) +// Warning: (ae-missing-release-tag) "AWSClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface AWSClusterDetails extends ClusterDetails { @@ -18,6 +18,8 @@ export interface AWSClusterDetails extends ClusterDetails { assumeRole?: string; } +// 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 { // (undocumented) @@ -59,11 +61,13 @@ export interface FetchResponseWrapper { responses: FetchResponse[]; } -// Warning: (ae-missing-release-tag) "KubernetesClustersSupplier" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "GKEClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface GKEClusterDetails extends ClusterDetails {} +// 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 KubernetesClustersSupplier { // (undocumented) @@ -142,11 +146,13 @@ export interface RouterOptions { logger: Logger_2; } -// Warning: (ae-missing-release-tag) "ServiceLocatorMethod" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "ServiceAccountClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface ServiceAccountClusterDetails extends ClusterDetails {} +// 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 type ServiceLocatorMethod = 'multiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index d5179fc07a..9060ce9239 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -19,16 +19,23 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator import { get, def } from 'bdd-lazy-var'; describe('AwsIamKubernetesAuthTranslator tests', () => { - let valid: boolean = true; let role: any = undefined; - let response: any = { + const credentials: any = { + accessKeyId: 'bloop', + secretAccessKey: 'omg-so-secret', + sessionToken: 'token', + }; + + let assumeResponse: any = { Credentials: { - AccessKeyId: 'bloop', - SecretAccessKey: 'omg-so-secret', - SessionToken: 'token', + AccessKeyId: credentials.accessKeyId, + SecretAccessKey: credentials.secretAccessKey, + SessionToken: credentials.sessionToken, }, }; + let credentialsResponse: any = new AWS.Credentials(credentials); + AWSMock.setSDKInstance(AWS); beforeEach(() => { @@ -41,13 +48,15 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { def('subject', () => { AWSMock.mock('STS', 'assumeRole', (_params: any, callback: Function) => { - callback(null, response); + callback(null, assumeResponse); }); const authTranslator = new AwsIamKubernetesAuthTranslator(); + jest - .spyOn(authTranslator, 'validCredentials') - .mockImplementation(() => valid); + .spyOn(authTranslator, 'awsGetCredentials') + .mockImplementation(async () => credentialsResponse); + return authTranslator.decorateClusterDetailsWithAuth({ assumeRole: role, name: 'test-cluster', @@ -86,16 +95,25 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { describe('When the role is invalid', () => { it('returns the original AWS credentials', async () => { - response = undefined; - + assumeResponse = undefined; await expect(get('subject')).rejects.toThrow(/Unable to assume role:/); }); }); }); - it('throws when unable to get aws credentials', async () => { - valid = false; - AWS.config.credentials = undefined; - await expect(get('subject')).rejects.toThrow('No AWS credentials found'); + describe('When no creds are returned from AWS', () => { + it('throws unable to get aws credentials', async () => { + credentialsResponse = new Error(); + await expect(get('subject')).rejects.toThrow('No AWS credentials found.'); + }); + }); + + describe('When invalid creds are returned from AWS', () => { + it('throws credentials are invalid to get aws credentials', async () => { + credentialsResponse = new AWS.Credentials(credentialsResponse); + await expect(get('subject')).rejects.toThrow( + 'Invalid AWS credentials found.', + ); + }); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index e9004818a2..c15022d504 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import AWS from 'aws-sdk'; +import AWS, { Credentials } from 'aws-sdk'; import { sign } from 'aws4'; import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; @@ -38,29 +38,43 @@ type SigningCreds = { export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { validCredentials(creds: SigningCreds): boolean { - if (!creds.accessKeyId || !creds.secretAccessKey || !creds.sessionToken) { + if ( + !creds?.accessKeyId || + !creds?.secretAccessKey || + !creds?.sessionToken + ) { return false; } return true; } + awsGetCredentials = async (): Promise => { + return new Promise((resolve, reject) => { + AWS.config.getCredentials(err => { + if (err) { + return reject(err); + } + + return resolve(AWS.config.credentials as Credentials); + }); + }); + }; + async getCredentials(assumeRole: string | undefined): Promise { return new Promise(async (resolve, reject) => { - await AWS.config.getCredentials(err => { - if (err) { - console.error('Unable to load aws config.'); - reject(err); - } - }); + const awsCreds = await this.awsGetCredentials(); + + if (!(awsCreds instanceof Credentials)) + return reject(Error('No AWS credentials found.')); let creds: SigningCreds = { - accessKeyId: AWS.config.credentials?.accessKeyId, - secretAccessKey: AWS.config.credentials?.secretAccessKey, - sessionToken: AWS.config.credentials?.sessionToken, + accessKeyId: awsCreds.accessKeyId, + secretAccessKey: awsCreds.secretAccessKey, + sessionToken: awsCreds.sessionToken, }; if (!this.validCredentials(creds)) - return reject(Error('No AWS credentials found.')); + return reject(Error('Invalid AWS credentials found.')); if (!assumeRole) return resolve(creds); try { From 65a4d898318e9e078199def138f6cc44b258f85b Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Tue, 20 Jul 2021 09:15:03 +0100 Subject: [PATCH 6/7] Refactoring the valid credentials method Signed-off-by: Nicolas Arnold --- .../AwsIamKubernetesAuthTranslator.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index c15022d504..101bed32a3 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -38,14 +38,9 @@ type SigningCreds = { export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { validCredentials(creds: SigningCreds): boolean { - if ( - !creds?.accessKeyId || - !creds?.secretAccessKey || - !creds?.sessionToken - ) { - return false; - } - return true; + return ((creds?.accessKeyId && + creds?.secretAccessKey && + creds?.sessionToken) as unknown) as boolean; } awsGetCredentials = async (): Promise => { From 7e84b1bd9f4fab234ff15b513fdf3c26afcade10 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Tue, 20 Jul 2021 14:20:06 +0100 Subject: [PATCH 7/7] Making changes backwards compatible by keeping the ClusterDetails object as an accepted type. Signed-off-by: Nicolas Arnold --- plugins/kubernetes-backend/api-report.md | 3 ++- plugins/kubernetes-backend/src/types/types.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 9f35fda79f..72d80050d8 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -123,7 +123,8 @@ export interface ObjectFetchParams { clusterDetails: | AWSClusterDetails | GKEClusterDetails - | ServiceAccountClusterDetails; + | ServiceAccountClusterDetails + | ClusterDetails; // (undocumented) customResources: CustomResource[]; // (undocumented) diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index c5f555d588..6409229968 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -30,7 +30,8 @@ export interface ObjectFetchParams { clusterDetails: | AWSClusterDetails | GKEClusterDetails - | ServiceAccountClusterDetails; + | ServiceAccountClusterDetails + | ClusterDetails; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[];