From 192d0173a0daef9fcc8d2fb8b8d4c05fc9363a33 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:15:52 +0100 Subject: [PATCH] use aws credentials provider Signed-off-by: Brian Fletcher --- .../AwsIamKubernetesAuthTranslator.test.ts | 63 ++++++++++++------- .../AwsIamKubernetesAuthTranslator.ts | 20 ++++-- .../KubernetesAuthTranslatorGenerator.test.ts | 15 +++-- .../KubernetesAuthTranslatorGenerator.ts | 4 +- .../src/service/KubernetesBuilder.ts | 1 + .../src/service/KubernetesFanOutHandler.ts | 5 ++ plugins/kubernetes-backend/src/types/types.ts | 2 + 7 files changed, 76 insertions(+), 34 deletions(-) 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 97272f9faa..e72291664b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; +import { ConfigReader } from '@backstage/config'; let presign = jest.fn(async () => ({ hostname: 'https://example.com', @@ -21,65 +22,79 @@ let presign = jest.fn(async () => ({ path: '/asdf', })); -const fromEnv = jest.fn(() => { - return { - AccessId: 'asdf', - }; -}); -const fromTemporaryCredentials = jest.fn(); +const credsManager = { + getCredentialProvider: async () => ({ + sdkCredentialProvider: { + AccessKeyId: 'asdf', + }, + }), +}; + +jest.mock('@backstage/integration-aws-node', () => ({ + DefaultAwsCredentialsManager: { + fromConfig: () => credsManager, + }, +})); + +const config = new ConfigReader({}); + jest.mock('@aws-sdk/signature-v4', () => ({ SignatureV4: jest.fn().mockImplementation(() => ({ presign, })), })); +const fromTemporaryCredentials = jest.fn(); jest.mock('@aws-sdk/credential-providers', () => ({ - fromEnv: () => fromEnv(), - fromTemporaryCredentials: (opts: any) => fromTemporaryCredentials(opts), + fromTemporaryCredentials: (opts: any) => { + console.log(`got: ${JSON.stringify(opts)}`); + return fromTemporaryCredentials(opts); + }, })); describe('AwsIamKubernetesAuthTranslator tests', () => { beforeEach(() => {}); it('returns a signed url for AWS credentials without assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', }); - expect(fromEnv).toHaveBeenCalledWith(); expect((await authPromise).serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); }); it('returns a signed url for AWS credentials with assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: 'asdf', + assumeRole: 'SomeRole', name: 'test-cluster', url: '', authProvider: 'aws', }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', }, - masterCredentials: fromEnv(), + masterCredentials: { + AccessKeyId: 'asdf', + }, params: { ExternalId: undefined, - RoleArn: 'asdf', + RoleArn: 'SomeRole', }, }); - expect((await authPromise).serviceAccountToken).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); }); it('returns a signed url for AWS credentials and passes the external id', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ assumeRole: 'SomeRole', @@ -88,19 +103,21 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', }, - masterCredentials: fromEnv(), + masterCredentials: { + AccessKeyId: 'asdf', + }, params: { ExternalId: 'external-id', RoleArn: 'SomeRole', }, }); - expect((await authPromise).serviceAccountToken).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); }); describe('When the credentials is failing', () => { @@ -110,7 +127,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { }); }); it('throws the right error', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); await expect( authTranslator.decorateClusterDetailsWithAuth({ name: 'test-cluster', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 40f74136ea..f5985b6a93 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -15,12 +15,14 @@ */ import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; -import { - fromEnv, - fromTemporaryCredentials, -} from '@aws-sdk/credential-providers'; +import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { SignatureV4 } from '@aws-sdk/signature-v4'; import { Sha256 } from '@aws-crypto/sha256-js'; +import { + AwsCredentialsManager, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; /** * @@ -41,13 +43,21 @@ const defaultRegion = 'us-east-1'; export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { + private readonly credsManager: AwsCredentialsManager; + constructor(opts: { config: Config }) { + this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); + } + private async getBearerToken( clusterName: string, assumeRole?: string, externalId?: string, ): Promise { + console.log(`here ${clusterName}, ${assumeRole} ${externalId}`); const region = process.env.AWS_REGION ?? defaultRegion; - let credentials = fromEnv(); + + let credentials = (await this.credsManager.getCredentialProvider()) + .sdkCredentialProvider; if (assumeRole) { credentials = fromTemporaryCredentials({ masterCredentials: credentials, 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 f8abdde9cf..a7e317e4d8 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -21,39 +21,44 @@ import { NoopKubernetesAuthTranslator } from './NoopKubernetesAuthTranslator'; import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; const logger = getVoidLogger(); +const config = new ConfigReader({}); describe('getKubernetesAuthTranslatorInstance', () => { const sut = KubernetesAuthTranslatorGenerator; it('can return an auth translator for google auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('google', { logger }); + sut.getKubernetesAuthTranslatorInstance('google', { logger, config }); expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for aws auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('aws', { logger }); + sut.getKubernetesAuthTranslatorInstance('aws', { logger, config }); expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for serviceAccount auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('serviceAccount', { logger }); + sut.getKubernetesAuthTranslatorInstance('serviceAccount', { + logger, + config, + }); expect(authTranslator instanceof NoopKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for oidc auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('oidc', { logger }); + sut.getKubernetesAuthTranslatorInstance('oidc', { logger, config }); expect(authTranslator instanceof OidcKubernetesAuthTranslator).toBe(true); }); it('throws an error when asked for an auth translator for an unsupported auth type', () => { expect(() => - sut.getKubernetesAuthTranslatorInstance('linode', { logger }), + sut.getKubernetesAuthTranslatorInstance('linode', { logger, config }), ).toThrow( 'authProvider "linode" has no KubernetesAuthTranslator associated with it', ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index f54e46efab..834de28c84 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -22,6 +22,7 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator import { GoogleServiceAccountAuthTranslator } from './GoogleServiceAccountAuthProvider'; import { AzureIdentityKubernetesAuthTranslator } from './AzureIdentityKubernetesAuthTranslator'; import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; +import { Config } from '@backstage/config'; /** * @@ -32,6 +33,7 @@ export class KubernetesAuthTranslatorGenerator { authProvider: string, options: { logger: Logger; + config: Config; }, ): KubernetesAuthTranslator { switch (authProvider) { @@ -39,7 +41,7 @@ export class KubernetesAuthTranslatorGenerator { return new GoogleKubernetesAuthTranslator(); } case 'aws': { - return new AwsIamKubernetesAuthTranslator(); + return new AwsIamKubernetesAuthTranslator(options); } case 'azure': { return new AzureIdentityKubernetesAuthTranslator(options.logger); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 367e5edef3..f1d9f0bae1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -121,6 +121,7 @@ export class KubernetesBuilder { const objectsProvider = this.getObjectsProvider({ logger, fetcher, + config, serviceLocator, customResources, objectTypesToFetch: this.getObjectTypesToFetch(), diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index f71c6506bd..75522c5887 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -48,6 +48,7 @@ import { CurrentResourceUsage, PodStatus, } from '@kubernetes/client-node'; +import { Config } from '@backstage/config'; const isRejected = ( input: PromiseSettledResult, @@ -196,14 +197,17 @@ export class KubernetesFanOutHandler { private readonly customResources: CustomResource[]; private readonly objectTypesToFetch: Set; private readonly authTranslators: Record; + private readonly config: Config; constructor({ + config, logger, fetcher, serviceLocator, customResources, objectTypesToFetch = DEFAULT_OBJECTS, }: KubernetesFanOutHandlerOptions) { + this.config = config; this.logger = logger; this.fetcher = fetcher; this.serviceLocator = serviceLocator; @@ -404,6 +408,7 @@ export class KubernetesFanOutHandler { provider, { logger: this.logger, + config: this.config, }, ); return this.authTranslators[provider]; diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 25c7abfd1f..31327b21ee 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -25,6 +25,7 @@ import type { KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-common'; +import { Config } from '@backstage/config'; /** * @@ -240,6 +241,7 @@ export interface AWSClusterDetails extends ClusterDetails { */ export interface KubernetesObjectsProviderOptions { logger: Logger; + config: Config; fetcher: KubernetesFetcher; serviceLocator: KubernetesServiceLocator; customResources: CustomResource[];