Merge pull request #16773 from RoadieHQ/k8s-plugin-crash
remove unhandled promise on bad creds
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Fixes bug whereby backstage crashes when bad credentials are provided to the kubernetes plugin.
|
||||
+26
@@ -17,6 +17,13 @@ import AWS from 'aws-sdk';
|
||||
import AWSMock from 'aws-sdk-mock';
|
||||
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
|
||||
|
||||
const awsError: AWS.AWSError = {
|
||||
code: '123',
|
||||
message: 'no way',
|
||||
name: 'nope',
|
||||
time: new Date(),
|
||||
};
|
||||
|
||||
describe('AwsIamKubernetesAuthTranslator tests', () => {
|
||||
let role: any = undefined;
|
||||
const credentials: any = {
|
||||
@@ -84,6 +91,25 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
|
||||
expect(response.serviceAccountToken).toBeDefined();
|
||||
});
|
||||
|
||||
describe('When the credentials is failing', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(AWS.config, 'getCredentials').mockImplementation(cb => {
|
||||
cb(awsError, null);
|
||||
});
|
||||
});
|
||||
it('throws the right error', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
await expect(
|
||||
authTranslator.decorateClusterDetailsWithAuth({
|
||||
assumeRole: role,
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
}),
|
||||
).rejects.toEqual(awsError);
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
+32
-34
@@ -55,46 +55,44 @@ export class AwsIamKubernetesAuthTranslator
|
||||
assumeRole?: string,
|
||||
externalId?: string,
|
||||
): Promise<SigningCreds> {
|
||||
return new Promise<SigningCreds>(async (resolve, reject) => {
|
||||
const awsCreds = await this.awsGetCredentials();
|
||||
const awsCreds = await this.awsGetCredentials();
|
||||
|
||||
if (!(awsCreds instanceof Credentials))
|
||||
return reject(Error('No AWS credentials found.'));
|
||||
if (!(awsCreds instanceof Credentials))
|
||||
throw new Error('No AWS credentials found.');
|
||||
|
||||
let creds: SigningCreds = {
|
||||
accessKeyId: awsCreds.accessKeyId,
|
||||
secretAccessKey: awsCreds.secretAccessKey,
|
||||
sessionToken: awsCreds.sessionToken,
|
||||
let creds: SigningCreds = {
|
||||
accessKeyId: awsCreds.accessKeyId,
|
||||
secretAccessKey: awsCreds.secretAccessKey,
|
||||
sessionToken: awsCreds.sessionToken,
|
||||
};
|
||||
|
||||
if (!this.validCredentials(creds))
|
||||
throw new Error('Invalid AWS credentials found.');
|
||||
if (!assumeRole) return creds;
|
||||
|
||||
try {
|
||||
const params: AWS.STS.Types.AssumeRoleRequest = {
|
||||
RoleArn: assumeRole,
|
||||
RoleSessionName: 'backstage-login',
|
||||
};
|
||||
if (externalId) params.ExternalId = externalId;
|
||||
|
||||
if (!this.validCredentials(creds))
|
||||
return reject(Error('Invalid AWS credentials found.'));
|
||||
if (!assumeRole) return resolve(creds);
|
||||
const assumedRole = await new AWS.STS().assumeRole(params).promise();
|
||||
|
||||
try {
|
||||
const params: AWS.STS.Types.AssumeRoleRequest = {
|
||||
RoleArn: assumeRole,
|
||||
RoleSessionName: 'backstage-login',
|
||||
};
|
||||
if (externalId) params.ExternalId = externalId;
|
||||
|
||||
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}`));
|
||||
if (!assumedRole.Credentials) {
|
||||
throw new Error(`No credentials returned for role ${assumeRole}`);
|
||||
}
|
||||
return resolve(creds);
|
||||
});
|
||||
|
||||
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}`);
|
||||
throw new Error(`Unable to assume role: ${e}`);
|
||||
}
|
||||
return creds;
|
||||
}
|
||||
async getBearerToken(
|
||||
clusterName: string,
|
||||
|
||||
@@ -49,6 +49,14 @@ import {
|
||||
PodStatus,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
const isRejected = (
|
||||
input: PromiseSettledResult<unknown>,
|
||||
): input is PromiseRejectedResult => input.status === 'rejected';
|
||||
|
||||
const isFulfilled = <T>(
|
||||
input: PromiseSettledResult<T>,
|
||||
): input is PromiseFulfilledResult<T> => input.status === 'fulfilled';
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
@@ -298,12 +306,12 @@ export class KubernetesFanOutHandler {
|
||||
auth: KubernetesRequestAuth,
|
||||
requestContext: ServiceLocatorRequestContext,
|
||||
) {
|
||||
const clusterDetails: ClusterDetails[] = await (
|
||||
const clusterDetails: ClusterDetails[] = (
|
||||
await this.serviceLocator.getClustersByEntity(entity, requestContext)
|
||||
).clusters;
|
||||
|
||||
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
|
||||
return await Promise.all(
|
||||
const promiseResults = await Promise.allSettled(
|
||||
clusterDetails.map(cd => {
|
||||
const kubernetesAuthTranslator: KubernetesAuthTranslator =
|
||||
this.getAuthTranslator(cd.authProvider);
|
||||
@@ -313,6 +321,11 @@ export class KubernetesFanOutHandler {
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
promiseResults.filter(isRejected).map(item => {
|
||||
this.logger.info(`Failed to decorate cluster details: ${item.reason}`);
|
||||
});
|
||||
return promiseResults.filter(isFulfilled).map(item => item.value);
|
||||
}
|
||||
|
||||
toObjectsByEntityResponse(
|
||||
|
||||
Reference in New Issue
Block a user