Skip to content

Commit

Permalink
Merge pull request #1245 from RoadieHQ/sc-18900-update-aws-providers
Browse files Browse the repository at this point in the history
Update AWS providers
  • Loading branch information
Xantier authored Feb 9, 2024
2 parents 1192950 + 17e76a9 commit db33632
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 14 deletions.
9 changes: 9 additions & 0 deletions .changeset/slow-files-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@roadiehq/catalog-backend-module-aws': major
---

Breaking change on entity output level, no code changes needed for most use cases.

Updating entities provided by DDB and Lambda providers to be of kind Resource instead of a Component.

Adding an additional EntityProvider to create entities from EC2 instances.
4 changes: 4 additions & 0 deletions packages/backend/src/plugins/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
AWSIAMRoleProvider,
AWSIAMRoleProcessor,
AWSEKSClusterProvider,
AWSEC2Provider,
} from '@roadiehq/catalog-backend-module-aws';
import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';
import { Duration } from 'luxon';
Expand Down Expand Up @@ -59,19 +60,22 @@ export default async function createPlugin(
const iamRoleProvider = AWSIAMRoleProvider.fromConfig(config, env);
const ddbTableProvider = AWSDynamoDbTableProvider.fromConfig(config, env);
const eksClusterProvider = AWSEKSClusterProvider.fromConfig(config, env);
const ec2Provider = AWSEC2Provider.fromConfig(config, env);

builder.addEntityProvider(s3Provider);
builder.addEntityProvider(lambdaProvider);
builder.addEntityProvider(iamUserProvider);
builder.addEntityProvider(iamRoleProvider);
builder.addEntityProvider(ddbTableProvider);
builder.addEntityProvider(eksClusterProvider);
builder.addEntityProvider(ec2Provider);
providers.push(s3Provider);
providers.push(lambdaProvider);
providers.push(iamUserProvider);
providers.push(iamRoleProvider);
providers.push(ddbTableProvider);
providers.push(eksClusterProvider);
providers.push(ec2Provider);

const useDdbData = config.has('dynamodbTableData');
if (useDdbData) {
Expand Down
4 changes: 4 additions & 0 deletions plugins/backend/catalog-backend-module-aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AWSLambdaFunctionProvider,
AWSS3BucketProvider,
AWSIAMUserProvider,
AWSEC2Provider,
} from '@roadiehq/catalog-backend-module-aws';

export default async function createPlugin(
Expand All @@ -19,14 +20,17 @@ export default async function createPlugin(
const s3Provider = AWSS3BucketProvider.fromConfig(config, env);
const lambdaProvider = AWSLambdaFunctionProvider.fromConfig(config, env);
const iamUserProvider = AWSIAMUserProvider.fromConfig(config, env);
const ec2Provider = AWSEC2Provider.fromConfig(config, env);

builder.addEntityProvider(s3Provider);
builder.addEntityProvider(lambdaProvider);
builder.addEntityProvider(iamUserProvider);
builder.addEntityProvider(ec2Provider);

s3Provider.run();
lambdaProvider.run();
iamUserProvider.run();
ec2Provider.run();

const { processingEngine, router } = await builder.build();
await processingEngine.start();
Expand Down
3 changes: 2 additions & 1 deletion plugins/backend/catalog-backend-module-aws/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@roadiehq/catalog-backend-module-aws",
"description": "A set of Backstage catalog processors for AWS",
"description": "A set of Backstage catalog providers for AWS",
"version": "1.3.18",
"main": "src/index.ts",
"types": "src/index.ts",
Expand Down Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@aws-sdk/client-cloudcontrol": "3.76.0",
"@aws-sdk/client-dynamodb": "3.76.0",
"@aws-sdk/client-ec2": "3.76.0",
"@aws-sdk/client-eks": "3.76.0",
"@aws-sdk/client-iam": "3.76.0",
"@aws-sdk/client-lambda": "3.76.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const ANNOTATION_AWS_IAM_USER_ARN = 'amazon.com/iam-user-arn';
export const ANNOTATION_AWS_LAMBDA_FUNCTION_ARN =
'amazon.com/lambda-function-arn';
export const ANNOTATION_AWS_S3_BUCKET_ARN = 'amazon.com/s3-bucket-arn';
export const ANNOTATION_AWS_EC2_INSTANCE_ID = 'amazon.com/ec2-instance-id';
export const ANNOTATION_AWS_DDB_TABLE_ARN = 'amazon.com/dynamo-db-table-arn';
export const ANNOTATION_AWS_EKS_CLUSTER_ARN = 'amazon.com/eks-cluster-arn';
export const ANNOTATION_ACCOUNT_ID = 'amazon.com/account-id';
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { ValueMapping } from '../types';

type DdbTableDataConfigOptions = {
entityKind?: string;
tableName: string;
identifierColumn?: string;
columnValueMapping?: { [key: string]: ValueMapping };
Expand Down Expand Up @@ -132,7 +133,7 @@ export class AWSDynamoDbTableDataProvider implements EntityProvider {
const entities =
tableRows.Items?.map(row => {
const o = {
kind: 'Component',
kind: this.tableDataConfig.entityKind ?? 'Component',
apiVersion: 'backstage.io/v1beta1',
metadata: {
annotations: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DynamoDB, paginateListTables } from '@aws-sdk/client-dynamodb';
import { Config } from '@backstage/config';
import * as winston from 'winston';
import { AWSEntityProvider } from './AWSEntityProvider';
import { ComponentEntity } from '@backstage/catalog-model';
import { ResourceEntity } from '@backstage/catalog-model';
import { ANNOTATION_AWS_DDB_TABLE_ARN } from '../annotations';
import { arnToName } from '../utils/arnToName';

Expand Down Expand Up @@ -62,7 +62,7 @@ export class AWSDynamoDbTableProvider extends AWSEntityProvider {

const tablePages = paginateListTables(paginatorConfig, {});

const ddbComponents: ComponentEntity[] = [];
let ddbComponents: ResourceEntity[] = [];
for await (const tablePage of tablePages) {
if (!tablePage.TableNames) {
continue;
Expand All @@ -75,8 +75,8 @@ export class AWSDynamoDbTableProvider extends AWSEntityProvider {
});
const table = tableDescriptionResult.Table;
if (table && table.TableName && table.TableArn) {
const component: ComponentEntity = {
kind: 'Component',
const component: ResourceEntity = {
kind: 'Resource',
apiVersion: 'backstage.io/v1beta1',
metadata: {
annotations: {
Expand All @@ -89,7 +89,6 @@ export class AWSDynamoDbTableProvider extends AWSEntityProvider {
spec: {
owner: this.accountId,
type: 'dynamo-db-table',
lifecycle: 'production',
},
};
return component;
Expand All @@ -100,7 +99,7 @@ export class AWSDynamoDbTableProvider extends AWSEntityProvider {
)
.filter(it => it)
.map(it => it!);
ddbComponents.concat(...newComponents);
ddbComponents = ddbComponents.concat(...newComponents);
}

await this.connection.applyMutation({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2024 Larder Software Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ResourceEntity } from '@backstage/catalog-model';
import { EC2 } from '@aws-sdk/client-ec2';
import * as winston from 'winston';
import { Config } from '@backstage/config';
import { AWSEntityProvider } from './AWSEntityProvider';
import { ANNOTATION_AWS_EC2_INSTANCE_ID } from '../annotations';

/**
* Provides entities from AWS Elastic Compute Cloud.
*/
export class AWSEC2Provider extends AWSEntityProvider {
static fromConfig(config: Config, options: { logger: winston.Logger }) {
const accountId = config.getString('accountId');
const roleArn = config.getString('roleArn');
const externalId = config.getOptionalString('externalId');
const region = config.getString('region');

return new AWSEC2Provider(
{ accountId, roleArn, externalId, region },
options,
);
}

getProviderName(): string {
return `aws-ec2-provider-${this.accountId}`;
}

async run(): Promise<void> {
if (!this.connection) {
throw new Error('Not initialized');
}

this.logger.info(`Providing ec2 resources from aws: ${this.accountId}`);
const ec2Resources: ResourceEntity[] = [];

const credentials = this.getCredentials();
const ec2 = new EC2({ credentials, region: this.region });

const defaultAnnotations = this.buildDefaultAnnotations();

const instances = await ec2.describeInstances({
Filters: [{ Name: 'instance-state-name', Values: ['running'] }],
});

for (const reservation of instances.Reservations || []) {
if (reservation.Instances) {
for (const instance of reservation.Instances) {
const instanceId = instance.InstanceId;
const resource: ResourceEntity = {
kind: 'Resource',
apiVersion: 'backstage.io/v1beta1',
metadata: {
annotations: {
...(await defaultAnnotations),
[ANNOTATION_AWS_EC2_INSTANCE_ID]: instanceId ?? 'unknown',
},
labels: instance.Tags?.reduce(
(acc: Record<string, string>, tag) => {
if (tag.Key && tag.Value) {
acc[tag.Key] = tag.Value;
}
return acc;
},
{},
),
name:
instanceId ??
`${reservation.ReservationId}-instance-${instance.InstanceId}`,
title:
instance?.Tags?.find(
tag => tag.Key === 'Name' || tag.Key === 'name',
)?.Value ?? undefined,
instancePlatform: instance.Platform,
instanceType: instance.InstanceType,
monitoringState: instance.Monitoring?.State,
instancePlacement: instance.Placement?.AvailabilityZone,
amountOfBlockDevices: instance.BlockDeviceMappings?.length ?? 0,
instanceCpuCores: instance.CpuOptions?.CoreCount,
instanceCpuThreadsPerCode: instance.CpuOptions?.ThreadsPerCore,
reservationId: reservation.ReservationId,
},
spec: {
owner: 'unknown',
type: 'ec2-instance',
},
};

ec2Resources.push(resource);
}
}
}

await this.connection.applyMutation({
type: 'full',
entities: ec2Resources.map(entity => ({
entity,
locationKey: this.getProviderName(),
})),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ describe('AWSLambdaFunctionProvider', () => {
FunctionName: 'my-function',
FunctionArn:
'arn:aws:lambda:eu-west-1:123456789012:function:my-function',
Runtime: 'nodejs14.x',
Role: 'arn:aws:iam::123456789012:role/lambdaRole',
Handler: 'src/functions/filename.handler',
CodeSize: 45096642,
Description: '',
Timeout: 30,
MemorySize: 1024,
PackageType: 'Zip',
Architectures: ['x86_64'],
EphemeralStorage: {
Size: 512,
},
},
],
});
Expand All @@ -89,9 +101,28 @@ describe('AWSLambdaFunctionProvider', () => {
entities: [
expect.objectContaining({
entity: expect.objectContaining({
kind: 'Component',
kind: 'Resource',
metadata: expect.objectContaining({
annotations: {
'amazon.com/iam-role-arn':
'arn:aws:iam::123456789012:role/lambdaRole',
'amazon.com/lambda-function-arn':
'arn:aws:lambda:eu-west-1:123456789012:function:my-function',
'backstage.io/managed-by-location':
'aws-lambda-function-123456789012:arn:aws:iam::123456789012:role/role1',
'backstage.io/managed-by-origin-location':
'aws-lambda-function-123456789012:arn:aws:iam::123456789012:role/role1',
'backstage.io/view-url':
'https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#/functions/my-function',
},
title: 'my-function',
architectures: ['x86_64'],
description: '',
ephemeralStorage: 512,
memorySize: 1024,
name: 'bc6fa48d05a0a464c5e2a5214985bd957578cd50314fc6076cef1845fadb3c8',
runtime: 'nodejs14.x',
timeout: 30,
}),
}),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { ANNOTATION_VIEW_URL, ComponentEntity } from '@backstage/catalog-model';
import { ANNOTATION_VIEW_URL, ResourceEntity } from '@backstage/catalog-model';
import { Lambda, paginateListFunctions } from '@aws-sdk/client-lambda';
import * as winston from 'winston';
import { Config } from '@backstage/config';
Expand Down Expand Up @@ -55,7 +55,7 @@ export class AWSLambdaFunctionProvider extends AWSEntityProvider {
`Providing lambda function resources from aws: ${this.accountId}`,
);

const lambdaComponents: ComponentEntity[] = [];
const lambdaComponents: ResourceEntity[] = [];

const credentials = this.getCredentials();
const lambda = new Lambda({ credentials, region: this.region });
Expand Down Expand Up @@ -83,17 +83,22 @@ export class AWSLambdaFunctionProvider extends AWSEntityProvider {
annotations[ANNOTATION_AWS_IAM_ROLE_ARN] = lambdaFunction.Role;
}
lambdaComponents.push({
kind: 'Component',
kind: 'Resource',
apiVersion: 'backstage.io/v1beta1',
metadata: {
annotations,
name: arnToName(lambdaFunction.FunctionArn),
title: lambdaFunction.FunctionName,
description: lambdaFunction.Description,
runtime: lambdaFunction.Runtime,
memorySize: lambdaFunction.MemorySize,
ephemeralStorage: lambdaFunction.EphemeralStorage?.Size,
timeout: lambdaFunction.Timeout,
architectures: lambdaFunction.Architectures,
},
spec: {
owner: 'unknown',
type: 'lambda-function',
lifecycle: 'production',
dependsOn: [],
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Larder Software Limited
* Copyright 2024 Larder Software Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,3 +21,4 @@ export { AWSIAMRoleProvider } from './AWSIAMRoleProvider';
export { AWSDynamoDbTableDataProvider } from './AWSDynamoDbTableDataProvider';
export { AWSDynamoDbTableProvider } from './AWSDynamoDbTableProvider';
export { AWSEKSClusterProvider } from './AWSEKSClusterProvider';
export { AWSEC2Provider } from './AWSEC2Provider';
Loading

0 comments on commit db33632

Please sign in to comment.