Skip to content

Commit

Permalink
Add AWS SQS, Redis, OpenSearch providers
Browse files Browse the repository at this point in the history
  • Loading branch information
Xantier committed Dec 18, 2024
1 parent 5d2b83b commit 4bf2752
Show file tree
Hide file tree
Showing 9 changed files with 1,382 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-horses-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/catalog-backend-module-aws': minor
---

Add SQS, Redis and OpenSearch providers
15 changes: 15 additions & 0 deletions packages/backend/src/plugins/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {
AWSRDSProvider,
AWSOrganizationAccountsProvider,
AWSSNSTopicProvider,
AWSSQSEntityProvider,
AWSOpenSearchEntityProvider,
AWSRedisEntityProvider,
} from '@roadiehq/catalog-backend-module-aws';
import { OktaOrgEntityProvider } from '@roadiehq/catalog-backend-module-okta';
import { Duration } from 'luxon';
Expand Down Expand Up @@ -72,6 +75,12 @@ export default async function createPlugin(
const eksClusterProvider = AWSEKSClusterProvider.fromConfig(config, env);
const ec2Provider = AWSEC2Provider.fromConfig(config, env);
const rdsProvider = AWSRDSProvider.fromConfig(config, env);
const sqsProvider = AWSSQSEntityProvider.fromConfig(config, env);
const openSearchProvider = AWSOpenSearchEntityProvider.fromConfig(
config,
env,
);
const redisProvider = AWSRedisEntityProvider.fromConfig(config, env);
const awsAccountsProvider = AWSOrganizationAccountsProvider.fromConfig(
config,
env,
Expand All @@ -86,6 +95,9 @@ export default async function createPlugin(
builder.addEntityProvider(eksClusterProvider);
builder.addEntityProvider(ec2Provider);
builder.addEntityProvider(rdsProvider);
builder.addEntityProvider(sqsProvider);
builder.addEntityProvider(openSearchProvider);
builder.addEntityProvider(redisProvider);
builder.addEntityProvider(awsAccountsProvider);
providers.push(s3Provider);
providers.push(lambdaProvider);
Expand All @@ -96,6 +108,9 @@ export default async function createPlugin(
providers.push(eksClusterProvider);
providers.push(ec2Provider);
providers.push(rdsProvider);
providers.push(sqsProvider);
providers.push(openSearchProvider);
providers.push(redisProvider);
providers.push(awsAccountsProvider);

const useDdbData = config.has('dynamodbTableData');
Expand Down
11 changes: 7 additions & 4 deletions plugins/backend/catalog-backend-module-aws/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,30 @@
"@aws-sdk/client-dynamodb": "^3.696.0",
"@aws-sdk/client-ec2": "^3.696.0",
"@aws-sdk/client-eks": "^3.696.0",
"@aws-sdk/client-elasticache": "^3.696.0",
"@aws-sdk/client-iam": "^3.696.0",
"@aws-sdk/client-lambda": "^3.696.0",
"@aws-sdk/client-opensearch": "^3.696.0",
"@aws-sdk/client-organizations": "^3.696.0",
"@aws-sdk/client-rds": "^3.696.0",
"@aws-sdk/client-s3": "^3.696.0",
"@aws-sdk/client-sts": "^3.696.0",
"@aws-sdk/client-sns": "^3.696.0",
"@aws-sdk/client-sqs": "^3.696.0",
"@aws-sdk/client-sts": "^3.696.0",
"@aws-sdk/credential-providers": "^3.696.0",
"@aws-sdk/lib-dynamodb": "^3.696.0",
"@backstage/integration-aws-node": "^0.1.13",
"@aws-sdk/util-arn-parser": "^3.76.0",
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-plugin-api": "^1.0.1",
"@backstage/catalog-client": "^1.8.0",
"@backstage/plugin-kubernetes-common": "^0.9.0",
"@backstage/catalog-model": "^1.7.1",
"@backstage/config": "^1.3.0",
"@backstage/errors": "^1.2.5",
"@backstage/integration-aws-node": "^0.1.13",
"@backstage/plugin-catalog-backend": "^1.28.0",
"@backstage/plugin-catalog-node": "^1.14.0",
"@backstage/plugin-kubernetes-common": "^0.9.0",
"@backstage/types": "^1.2.0",
"@aws-sdk/util-arn-parser": "^3.76.0",
"link2aws": "^1.0.18",
"lodash": "^4.17.21",
"p-limit": "^3.0.2",
Expand Down
5 changes: 5 additions & 0 deletions plugins/backend/catalog-backend-module-aws/src/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ 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_RDS_INSTANCE_ARN = 'amazon.com/rds-instance-arn';
export const ANNOTATION_AWS_DDB_TABLE_ARN = 'amazon.com/dynamo-db-table-arn';
export const ANNOTATION_AWS_OPEN_SEARCH_ARN =
'amazon.com/open-search-domain-arn';
export const ANNOTATION_AWS_REDIS_CLUSTER_ARN =
'amazon.com/elasticache-redis-arn';
export const ANNOTATION_AWS_SQS_QUEUE_ARN = 'amazon.com/sqs-queue-arn';
export const ANNOTATION_AWS_EKS_CLUSTER_ARN = 'amazon.com/eks-cluster-arn';
export const ANNOTATION_AWS_EKS_CLUSTER_VERSION =
'amazon.com/eks-cluster-version';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* 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 { OpenSearch } from '@aws-sdk/client-opensearch';
import * as winston from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { AWSEntityProvider } from './AWSEntityProvider';
import { ownerFromTags, relationshipsFromTags } from '../utils/tags';
import { CatalogApi } from '@backstage/catalog-client';
import { AccountConfig, DynamicAccountConfig } from '../types';
import { duration } from '../utils/timer';
import { ANNOTATION_AWS_OPEN_SEARCH_ARN } from '../annotations';

/**
* Provides entities from AWS OpenSearch service.
*/
export class AWSOpenSearchEntityProvider extends AWSEntityProvider {
private readonly opensearchTypeValue: string;

static fromConfig(
config: Config,
options: {
logger: winston.Logger | LoggerService;
catalogApi?: CatalogApi;
providerId?: string;
ownerTag?: string;
useTemporaryCredentials?: boolean;
},
) {
const accountId = config.getString('accountId');
const roleName = config.getString('roleName');
const roleArn = config.getOptionalString('roleArn');
const externalId = config.getOptionalString('externalId');
const region = config.getString('region');

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

constructor(
account: AccountConfig,
options: {
logger: winston.Logger | LoggerService;
catalogApi?: CatalogApi;
providerId?: string;
ownerTag?: string;
useTemporaryCredentials?: boolean;
},
) {
super(account, options);
this.opensearchTypeValue = 'opensearch-domain';
}

getProviderName(): string {
return `aws-opensearch-domain-${this.providerId ?? 0}`;
}

private async getOpenSearchClient(
dynamicAccountConfig?: DynamicAccountConfig,
) {
const { region } = this.getParsedConfig(dynamicAccountConfig);
const credentials = this.useTemporaryCredentials
? this.getCredentials(dynamicAccountConfig)
: await this.getCredentialsProvider();
return this.useTemporaryCredentials
? new OpenSearch({ credentials, region })
: new OpenSearch(credentials);
}

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

const startTimestamp = process.hrtime();
const { accountId } = this.getParsedConfig(dynamicAccountConfig);

this.logger.info(
`Providing OpenSearch domain resources from AWS: ${accountId}`,
);
const opensearchResources: ResourceEntity[] = [];

const openSearchClient = await this.getOpenSearchClient(
dynamicAccountConfig,
);

const defaultAnnotations = await this.buildDefaultAnnotations(
dynamicAccountConfig,
);

// Fetch all OpenSearch domains
const domainList = await openSearchClient.listDomainNames();

if (domainList.DomainNames) {
for (const domain of domainList.DomainNames) {
const domainName = domain.DomainName || 'unknown';

const domainDetails = await openSearchClient.describeDomain({
DomainName: domainName,
});

const domainStatus = domainDetails.DomainStatus;
const domainArn = domainStatus?.ARN;
const tags = domainArn
? await openSearchClient.listTags({ ARN: domainArn })
: undefined;

const endpoint = domainStatus?.Endpoint || '';
const engineVersion = domainStatus?.EngineVersion || '';
const storageType = domainStatus?.EBSOptions?.EBSEnabled
? `EBS-${domainStatus.EBSOptions.VolumeType}`
: 'Instance Storage';

const resource: ResourceEntity = {
kind: 'Resource',
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: domainName.toLowerCase().replace(/[^a-zA-Z0-9\-]/g, '-'),
title: domainName,
annotations: {
...defaultAnnotations,
[ANNOTATION_AWS_OPEN_SEARCH_ARN]: domainArn ?? '',
},
labels: {
'aws-opensearch-region': this.region,
},
endpoint,
engineVersion,
storageType,
},
spec: {
owner: ownerFromTags(tags?.TagList || [], this.getOwnerTag()),
...relationshipsFromTags(tags?.TagList || []),
type: this.opensearchTypeValue,
},
};

opensearchResources.push(resource);
}
}

await this.connection.applyMutation({
type: 'full',
entities: opensearchResources.map(entity => ({
entity,
locationKey: this.getProviderName(),
})),
});

this.logger.info(
`Finished providing ${opensearchResources.length} OpenSearch domain resources from AWS: ${accountId}`,
{ run_duration: duration(startTimestamp) },
);
}
}
Loading

0 comments on commit 4bf2752

Please sign in to comment.