From 67c2f519345c709a3f339e1cafe5a4b13b79ea05 Mon Sep 17 00:00:00 2001 From: Florian JUDITH Date: Mon, 28 Oct 2024 23:32:19 -0400 Subject: [PATCH] Added support for @backstage/integration-aws-node for request authentication - Implement `@backstage/integration-aws-node` for aws-sdk clients authentication - Added API report `yarn build:api-reports:only` - Applied Prettier code formatting `yarn prettier write` --- .../plugins/aws-apps-backend/README.md | 29 +- .../plugins/aws-apps-backend/api-report.md | 286 +++++++++ .../plugins/aws-apps-backend/package.json | 4 +- .../aws-apps-backend/src/api/AwsAppsApi.ts | 492 +++++++++------- .../aws-apps-backend/src/api/aws-audit.ts | 15 +- .../aws-apps-backend/src/api/aws-auth.ts | 110 ++-- .../aws-apps-backend/src/api/aws-platform.ts | 207 +++---- .../aws-apps-backend/src/api/git-api.ts | 58 +- .../aws-apps-backend/src/api/github-api.ts | 306 +++++----- .../aws-apps-backend/src/api/gitlab-api.ts | 406 ++++++------- .../plugins/aws-apps-backend/src/api/index.ts | 4 +- .../plugins/aws-apps-backend/src/index.ts | 3 +- .../plugins/aws-apps-backend/src/plugin.ts | 19 +- .../src/service/router.test.ts | 60 +- .../aws-apps-backend/src/service/router.ts | 341 ++++++----- .../src/service/standaloneServer.ts | 46 -- .../plugins/aws-apps-common/.eslintrc.js | 1 + .../plugins/aws-apps-common/README.md | 6 +- .../plugins/aws-apps-common/api-report.md | 548 ++++++++++++++++++ .../plugins/aws-apps-common/config.d.ts | 16 +- .../plugins/aws-apps-common/package.json | 8 +- .../AWSEnvironmentEntityV1.schema.json | 4 +- .../src/entities/AWSEnvironmentEntityV1.ts | 104 ++-- ...AWSEnvironmentProviderEntityV1.schema.json | 122 ++-- .../AWSEnvironmentProviderEntityV1.ts | 6 +- .../aws-apps-common/src/helpers/constants.ts | 13 + .../plugins/aws-apps-common/src/index.ts | 8 +- .../aws-apps-common/src/permissions.ts | 2 + .../aws-apps-common/src/types/AWSResource.ts | 25 +- .../src/types/AWSServiceResources.ts | 4 +- .../src/types/AWSUIInterfaces.ts | 174 +++--- .../src/types/AppPromoTypes.ts | 6 +- .../src/types/PlatformTypes.ts | 11 +- .../src/types/SCMBackendAPI.ts | 72 +-- .../src/types/git-providers.ts | 10 +- .../aws-apps-common/src/types/index.ts | 6 +- .../aws-apps-common/src/utils/git-util.ts | 156 +++-- .../plugins/aws-apps-demo/README.md | 24 +- .../plugins/aws-apps-demo/package.json | 11 +- .../AWSAppsHomePage/AWSAppsHomePage.tsx | 7 +- .../components/OPAHomePage/OPAHomePage.tsx | 9 +- .../src/components/OPALogoFull.tsx | 88 +-- .../src/components/theme/aws-theme.ts | 6 +- .../src/components/theme/customer-theme.ts | 2 +- .../src/components/theme/opa-theme.ts | 6 +- backstage-plugins/plugins/aws-apps/README.md | 15 +- .../plugins/aws-apps/package.json | 18 +- .../plugins/aws-apps/src/api/OPAApi.ts | 45 +- .../plugins/aws-apps/src/api/OPAApiClient.ts | 156 +++-- .../AdvancedEntityTypePicker.tsx | 8 +- .../AppCatalogPage/AppCatalogPage.tsx | 3 +- .../components/AppCatalogPage/awsColumns.tsx | 203 ++----- .../AppConfigCard/AppConfigCard.tsx | 51 +- .../components/AppLinksCard/AppLinksCard.tsx | 5 +- .../AppLinksCard/AppLinksEmptyState.tsx | 7 +- .../src/components/AppLinksCard/IconLink.tsx | 10 +- .../AppLinksCard/useDynamicColumns.tsx | 4 +- .../components/AppPromoCard/AppPromoCard.tsx | 305 +++++----- .../AppPromoCard/AwsEksEnvPromoDialog.tsx | 67 ++- .../components/AppStateCard/AppStateCard.tsx | 19 +- .../AppStateCardCloudFormation.tsx | 188 +++--- .../src/components/AppView/AppView.tsx | 88 +-- .../AwsEnvironmentProviderCard.tsx | 284 +++++---- .../AwsEnvironmentProviderSelectorDialog.tsx | 38 +- .../components/CICDContent/CICDContent.tsx | 61 +- .../CloudwatchLogsTable.tsx | 86 ++- .../DeleteComponentCard.tsx | 334 ++++++----- .../DeleteEnvironmentCard.tsx | 145 +++-- .../DeleteProviderCard/DeleteProviderCard.tsx | 242 ++++---- .../EnvironmentInfoCard.tsx | 42 +- .../EnvironmentSelector.tsx | 24 +- .../GeneralInfoCard/GeneralInfoCard.tsx | 82 ++- .../components/GenericTable/GenericTable.tsx | 1 - .../InfrastructureCard/InfrastructureCard.tsx | 66 ++- .../ResourceDetailsDialog.tsx | 27 +- .../InfrastructureCard/ServiceComponent.tsx | 71 ++- .../K8sAppStateCard/K8sAppStateCard.tsx | 415 +++++++------ .../src/components/LabelTable/LabelTable.tsx | 2 +- .../ProviderInfoCard/ProviderInfoCard.tsx | 68 +-- .../ResourceBindingCard/ResourceBinding.tsx | 327 ++++++----- .../ResourceSelectorDialog.tsx | 145 ++--- .../common/SecretStringComponent.tsx | 4 +- .../plugins/aws-apps/src/helpers/constants.ts | 14 +- .../aws-apps/src/helpers/date-utils.ts | 4 +- .../plugins/aws-apps/src/helpers/util.ts | 14 +- .../plugins/aws-apps/src/hooks/useAwsApp.tsx | 374 ++++++------ .../src/hooks/useCancellablePromise.ts | 35 +- .../plugins/aws-apps/src/index.ts | 2 +- .../AwsComponentPage/AwsComponentPage.tsx | 30 +- .../src/pages/AwsECSAppPage/AwsECSAppPage.tsx | 21 +- .../AwsECSEnvironmentProviderPage.tsx | 9 +- .../src/pages/AwsEKSAppPage/AwsEKSAppPage.tsx | 19 +- .../AwsEKSEnvironmentProviderPage.tsx | 8 +- .../AwsEnvironmentPage/AwsEnvironmentPage.tsx | 13 +- .../AwsEnvironmentProviderPage.tsx | 8 +- .../AwsRDSResourcePage/AwsRDSResourcePage.tsx | 12 +- .../pages/AwsResourcePage/AwsResourcePage.tsx | 6 +- .../AwsS3ResourcePage/AwsS3ResourcePage.tsx | 12 +- .../AwsSecretsManagerResourcePage.tsx | 12 +- .../AwsServerlessAppPage.tsx | 22 +- .../AwsServerlessEnvironmentProviderPage.tsx | 8 +- .../plugins/aws-apps/src/plugin.ts | 12 +- .../package.json | 2 +- .../src/index.ts | 2 - .../src/module.ts | 14 +- .../AWSEnvironmentEntitiesProcessor.ts | 207 +++---- ...AWSEnvironmentProviderEntitiesProcessor.ts | 6 +- .../README.md | 31 +- .../api-report.md | 139 +++++ .../package.json | 10 +- .../create-repoAccesstoken.ts | 27 +- .../actions/create-repo-access-token/index.ts | 2 +- .../create-s3-bucket/create-s3-bucket.ts | 24 +- .../actions/create-secret/create-secret.ts | 19 +- .../src/actions/create-secret/index.ts | 2 +- .../get-component-info/get-component-info.ts | 7 +- .../get-env-providers/get-env-providers.ts | 144 +++-- .../src/actions/get-env-providers/index.ts | 3 +- .../get-platform-metadata.ts | 15 +- .../actions/get-platform-metadata/index.ts | 1 - .../get-platform-parameters.ts | 74 +-- .../get-ssm-parameters/get-ssm-parameters.ts | 116 ++-- .../src/example/template.yaml | 14 +- .../src/helpers/action-context.ts | 102 +++- .../src/helpers/util.ts | 2 +- .../src/index.ts | 4 +- .../src/module.ts | 44 ++ .../src/types.ts | 6 +- 128 files changed, 5218 insertions(+), 3931 deletions(-) create mode 100644 backstage-plugins/plugins/aws-apps-backend/api-report.md delete mode 100644 backstage-plugins/plugins/aws-apps-backend/src/service/standaloneServer.ts create mode 100644 backstage-plugins/plugins/aws-apps-common/.eslintrc.js create mode 100644 backstage-plugins/plugins/aws-apps-common/api-report.md create mode 100644 backstage-plugins/plugins/aws-apps-common/src/helpers/constants.ts create mode 100644 backstage-plugins/plugins/scaffolder-backend-module-aws-apps/api-report.md create mode 100644 backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/module.ts diff --git a/backstage-plugins/plugins/aws-apps-backend/README.md b/backstage-plugins/plugins/aws-apps-backend/README.md index 1015200f..299877ba 100644 --- a/backstage-plugins/plugins/aws-apps-backend/README.md +++ b/backstage-plugins/plugins/aws-apps-backend/README.md @@ -1,21 +1,22 @@ - + # OPA on AWS Backend -This is the backend part of the OPA on AWS plugin. Its key responsibilities: +This is the backend part of the OPA on AWS plugin. Its key responsibilities: 1. **Catalog contributions** - the plugin provides the AWSEnvironment and AWSEnvironmentProvider entity Kinds, including processing and validation of the entities. 2. **Authentication / Authorization** - the plugin assumes defined roles with permisisons for provisioning infrastructure resources for a target environment account. 3. **Audit** - the plugin provides services to record requested actions, user id and IAM role, timestamps, success/failure results, and additional information for the purpose of capturing audit-level information about the actions performed by the AWS Apps Backstage plugin against AWS. -4. **Proxying AWS requests** - the plugin provides API endpoints for specific AWS service actions. It receives requests on these endpoints, validates the request, and proxies the request and response between Backstage and a specified AWS account and region. +4. **Proxying AWS requests** - the plugin provides API endpoints for specific AWS service actions. It receives requests on these endpoints, validates the request, and proxies the request and response between Backstage and a specified AWS account and region. ## Installation ```sh # From your Backstage root directory -yarn add --cwd packages/backend @aws/plugin-aws-apps-backend-for-backstage@0.2.0 +yarn add --cwd packages/backend @alithya-oss/plugin-aws-apps-backend@0.2.0 ``` ## Configuration @@ -24,15 +25,15 @@ Setup for the AWS Apps backend requires a router for Backstage, making the catal ### Configure a router -Create a `awsApps.ts` file in the `packages/backend/src/plugins/`directory. This file creates a router for the OPA on AWS backend. +Create a `awsApps.ts` file in the `packages/backend/src/plugins/`directory. This file creates a router for the OPA on AWS backend. ```ts // packages/backend/src/plugins/awsApps.ts -import {createRouter} from '@aws/plugin-aws-apps-backend-for-backstage' +import { createRouter } from '@alithya-oss/plugin-aws-apps-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import {DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; export default async function createPlugin({ logger, @@ -92,13 +93,13 @@ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; import { ScaffolderEntitiesProcessor } from '@backstage/plugin-catalog-backend-module-scaffolder-entity-model'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -+ import { AWSEnvironmentEntitiesProcessor, AWSEnvironmentProviderEntitiesProcessor} from '@aws/plugin-aws-apps-backend-for-backstage'; ++ import { AWSEnvironmentEntitiesProcessor, AWSEnvironmentProviderEntitiesProcessor} from '@alithya-oss/plugin-aws-apps-backend'; export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); - + builder.addProcessor(new ScaffolderEntitiesProcessor()); + // Custom processors @@ -114,7 +115,7 @@ export default async function createPlugin( ### Permission Framework Policy -The OPA on AWS backend plugin leverages the [Backstage permissions framework](https://backstage.io/docs/permissions/overview) to contribute a permission decision for access to audit entries. If you would like to implement a policy for your Backstage instance to control access to audit entries you will start with the [Permission framework getting started documentation](https://backstage.io/docs/permissions/getting-started) to set up the base framework. +The OPA on AWS backend plugin leverages the [Backstage permissions framework](https://backstage.io/docs/permissions/overview) to contribute a permission decision for access to audit entries. If you would like to implement a policy for your Backstage instance to control access to audit entries you will start with the [Permission framework getting started documentation](https://backstage.io/docs/permissions/getting-started) to set up the base framework. With the framework in place, you can leverage the `readOpaAppAuditPermission` permission in your policy definition to restrict access to audit entries. ```ts @@ -133,8 +134,8 @@ export class permissionPolicy implements PermissionPolicy { const VILLIANS_GROUP = stringifyEntityRef({ kind: 'Group', namespace: DEFAULT_NAMESPACE, name: "villians" }); const ownershipGroups = user?.identity.ownershipEntityRefs || []; if ( - isPermission(request.permission, readOpaAppAuditPermission) && - ownershipGroups.length === 1 && + isPermission(request.permission, readOpaAppAuditPermission) && + ownershipGroups.length === 1 && ownershipGroups.includes(VILLIANS_GROUP) ) { return { result: AuthorizationResult.DENY }; @@ -146,4 +147,4 @@ export class permissionPolicy implements PermissionPolicy { ``` -Additional permission decisions and resources are planned for future releases. \ No newline at end of file +Additional permission decisions and resources are planned for future releases. diff --git a/backstage-plugins/plugins/aws-apps-backend/api-report.md b/backstage-plugins/plugins/aws-apps-backend/api-report.md new file mode 100644 index 00000000..5d9cd172 --- /dev/null +++ b/backstage-plugins/plugins/aws-apps-backend/api-report.md @@ -0,0 +1,286 @@ +## API Report File for "@aws/plugin-aws-apps-backend-for-backstage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AuthService } from '@backstage/backend-plugin-api'; +import { AwsCredentialIdentity } from '@aws-sdk/types'; +import { AWSServiceResources } from '@aws/plugin-aws-apps-common-for-backstage'; +import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { CreateBucketCommandOutput } from '@aws-sdk/client-s3'; +import { CreateSecretCommandOutput } from '@aws-sdk/client-secrets-manager'; +import { CreateStackCommandOutput } from '@aws-sdk/client-cloudformation'; +import { DeleteStackCommandOutput } from '@aws-sdk/client-cloudformation'; +import { DescribeClusterCommandOutput } from '@aws-sdk/client-eks'; +import { DescribeLogGroupsCommandOutput } from '@aws-sdk/client-cloudwatch-logs'; +import { DescribeLogStreamsCommandOutput } from '@aws-sdk/client-cloudwatch-logs'; +import { DescribeStackEventsCommandOutput } from '@aws-sdk/client-cloudformation'; +import { DescribeStacksCommandOutput } from '@aws-sdk/client-cloudformation'; +import { DescribeTaskDefinitionCommandOutput } from '@aws-sdk/client-ecs'; +import { DescribeTasksCommandOutput } from '@aws-sdk/client-ecs'; +import express from 'express'; +import { GetLogEventsCommandOutput } from '@aws-sdk/client-cloudwatch-logs'; +import { GetLogRecordCommandOutput } from '@aws-sdk/client-cloudwatch-logs'; +import { GetParameterCommandOutput } from '@aws-sdk/client-ssm'; +import { GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; +import { HeadObjectCommandOutput } from '@aws-sdk/client-s3'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; +import { InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { ListGroupResourcesCommandOutput } from '@aws-sdk/client-resource-groups'; +import { ListTasksCommandOutput } from '@aws-sdk/client-ecs'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Parameter } from '@aws-sdk/client-cloudformation'; +import { PermissionsService } from '@backstage/backend-plugin-api'; +import { PutItemCommandOutput } from '@aws-sdk/client-dynamodb'; +import { PutSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; +import { RegisterTaskDefinitionCommandOutput } from '@aws-sdk/client-ecs'; +import { ScanCommandOutput } from '@aws-sdk/client-dynamodb'; +import { TaskDefinition } from '@aws-sdk/client-ecs'; +import { UpdateServiceCommandOutput } from '@aws-sdk/client-ecs'; +import { UpdateStackCommandOutput } from '@aws-sdk/client-cloudformation'; +import { UserEntity } from '@backstage/catalog-model'; +import { UserInfoService } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +export class AwsAppsApi { + constructor( + config: Config, + logger: LoggerService, + awsRegion: string, + awsAccount: string, + ); + // (undocumented) + callLambda(functionName: string, body: string): Promise; + // (undocumented) + createS3Bucket( + bucketName: string, + tags?: { + Key: string; + Value: string | number | boolean; + }[], + ): Promise; + // (undocumented) + createSecret( + secretName: string, + description: string, + tags?: { + Key: string; + Value: string | number | boolean; + }[], + ): Promise; + createStack( + componentName: string, + stackName: string, + s3BucketName: string, + cfFileName: string, + providerName: string, + parameters?: Parameter[], + ): Promise; + deleteStack(stackName: string): Promise; + describeClusterTasks( + clusterName: string, + taskArns: string[], + ): Promise; + describeStack(stackName: string): Promise; + describeStackEvents( + stackName: string, + ): Promise; + describeTaskDefinition( + taskDefinitionArn: string, + ): Promise; + // (undocumented) + doesS3FileExist( + bucketName: string, + fileName: string, + ): Promise; + getCategorizedResources(resourceGroup: string): Promise; + // (undocumented) + getDynamodbTable( + tableName: string, + appName: string, + timeFrame: number, + ): Promise; + getEcsServiceTask( + clusterName: string, + serviceName: string, + ): Promise; + getEksCluster(clusterName: string): Promise; + getLogGroupEvents( + logGroupName: string, + logStreamName: string, + startFromHead?: boolean, + ): Promise; + getLogGroups(logPrefix: string): Promise; + // (undocumented) + getLogRecord(logRecordPointer: string): Promise; + getLogStreams(logGroupName: string): Promise; + getResourceGroupResources( + resourceGroupName: string, + ): Promise; + getSecretValue(secretArn: string): Promise; + getSSMParameter(ssmParamName: string): Promise; + // (undocumented) + putDynamodbTableData(data: DynamoDBTableData): Promise; + // (undocumented) + putSecretValue( + secretArn: string, + secretValue: string, + ): Promise; + registerTaskDefinition( + taskDefinition: TaskDefinition, + ): Promise; + updateServiceTask( + clusterName: string, + serviceName: string, + taskDefinition: string, + restart: boolean, + numberOfTasks?: number | undefined, + ): Promise; + updateStack( + componentName: string, + stackName: string, + s3BucketName: string, + cfFileName: string, + providerName: string, + parameters?: Parameter[], + ): Promise; +} + +// @public +const awsAppsPlugin: BackendFeatureCompat; +export default awsAppsPlugin; + +// @public (undocumented) +export interface AwsAuditRequest { + // (undocumented) + actionName: string; + // (undocumented) + actionType: string; + // (undocumented) + apiClient: AwsAppsApi; + // (undocumented) + appName: string; + // (undocumented) + awsAccount: string; + // (undocumented) + awsRegion: string; + // (undocumented) + envProviderName: string; + // (undocumented) + envProviderPrefix: string; + // (undocumented) + logger: LoggerService; + // (undocumented) + message?: string; + // (undocumented) + owner: string; + // (undocumented) + requestArgs?: string; + // (undocumented) + requester: string; + // (undocumented) + roleArn: string; + // (undocumented) + status: string; +} + +// @public (undocumented) +export interface AwsAuditResponse { + // (undocumented) + message: string; + // (undocumented) + status: string; +} + +// @public (undocumented) +export interface AwsAuthResponse { + // (undocumented) + account: string; + // (undocumented) + credentials: AwsCredentialIdentity; + // (undocumented) + owner?: string; + // (undocumented) + region: string; + // (undocumented) + requester: string; + // (undocumented) + roleArn: string; +} + +// @public (undocumented) +export function createAuditRecord({ + envProviderPrefix, + envProviderName, + appName, + apiClient, + roleArn, + awsRegion, + awsAccount, + requester, + owner, + actionType, + actionName, + requestArgs, + status, + message, +}: AwsAuditRequest): Promise; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export type DynamoDBTableData = { + tableName: string; + recordId: string; + origin: string; + prefix: string; + appName: string; + environmentProviderName: string; + actionType: string; + name: string; + initiatedBy: string; + owner: string; + assumedRole: string; + targetAccount: string; + targetRegion: string; + request: string; + status: string; + message: string; +}; + +// @public (undocumented) +export function getAWScreds( + config: Config, + logger: LoggerService, + accountId: string, + region: string, + prefix: string, + providerName: string, + user?: UserEntity, + userIdentity?: BackstageUserInfo, +): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + auth: AuthService; + // (undocumented) + catalogApi: CatalogApi; + // (undocumented) + config: Config; + // (undocumented) + httpAuth: HttpAuthService; + // (undocumented) + logger: LoggerService; + // (undocumented) + permissions: PermissionsService; + // (undocumented) + userInfo: UserInfoService; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/backstage-plugins/plugins/aws-apps-backend/package.json b/backstage-plugins/plugins/aws-apps-backend/package.json index f5e0558c..574512cc 100644 --- a/backstage-plugins/plugins/aws-apps-backend/package.json +++ b/backstage-plugins/plugins/aws-apps-backend/package.json @@ -25,7 +25,9 @@ "backstage": { "role": "backend-plugin", "pluginId": "aws-apps-backend", - "pluginPackages": ["@backstage/plugin-catalog"] + "pluginPackages": [ + "@aws/plugin-aws-apps-backend-for-backstage" + ] }, "scripts": { "start": "backstage-cli package start", diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/AwsAppsApi.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/AwsAppsApi.ts index 8147b78b..1c5a5547 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/AwsAppsApi.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/AwsAppsApi.ts @@ -14,7 +14,7 @@ import { DescribeStacksCommandOutput, Parameter, UpdateStackCommand, - UpdateStackCommandOutput + UpdateStackCommandOutput, } from '@aws-sdk/client-cloudformation'; import { CloudWatchLogsClient, @@ -65,12 +65,7 @@ import { DescribeClusterCommandOutput, EKSClient, } from '@aws-sdk/client-eks'; -import { - InvokeCommand, - InvokeCommandInput, - InvokeCommandOutput, - LambdaClient -} from '@aws-sdk/client-lambda'; +import { InvokeCommand, InvokeCommandInput, InvokeCommandOutput, LambdaClient } from '@aws-sdk/client-lambda'; import { ListGroupResourcesCommand, ListGroupResourcesCommandInput, @@ -106,34 +101,38 @@ import { SSMClient, } from '@aws-sdk/client-ssm'; -import { AwsCredentialIdentity } from '@aws-sdk/types'; import { parse as parseArn } from '@aws-sdk/util-arn-parser'; import { AWSServiceResources } from '@aws/plugin-aws-apps-common-for-backstage'; import { LoggerService } from '@backstage/backend-plugin-api'; -export type DynamoDBTableData = { - tableName: string - recordId: string - origin: string - prefix: string - appName:string - environmentProviderName: string - actionType: string - name: string - initiatedBy: string - owner: string - assumedRole: string - targetAccount: string - targetRegion: string - request: string - status: string - message: string -} +import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; +/** @public */ +export type DynamoDBTableData = { + tableName: string; + recordId: string; + origin: string; + prefix: string; + appName: string; + environmentProviderName: string; + actionType: string; + name: string; + initiatedBy: string; + owner: string; + assumedRole: string; + targetAccount: string; + targetRegion: string; + request: string; + status: string; + message: string; +}; + +/** @public */ export class AwsAppsApi { public constructor( + private readonly config: Config, private readonly logger: LoggerService, - private readonly awsCredentials: AwsCredentialIdentity, private readonly awsRegion: string, private readonly awsAccount: string, ) { @@ -155,19 +154,23 @@ export class AwsAppsApi { public async getEcsServiceTask(clusterName: string, serviceName: string): Promise { this.logger.info('Calling getEcsServiceTask'); // resolve ECS cluster param to value - const clusterRef = await this.getSSMParameter(clusterName) + const clusterRef = await this.getSSMParameter(clusterName); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ECSClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: ListTasksCommandInput = { - cluster: clusterRef.Parameter?.Value?.toString() || "", + cluster: clusterRef.Parameter?.Value?.toString() ?? '', serviceName: serviceName, }; const command = new ListTasksCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Describes tasks of an ECS Service @@ -176,26 +179,30 @@ export class AwsAppsApi { * Describe the active running tasks of a giving cluster * * @param clusterName - The ECS Cluster name - * @param taskArns - List of Task arns + * @param taskArns - List of Task ARNs * @returns The DescribeTasksCommand object * */ public async describeClusterTasks(clusterName: string, taskArns: string[]): Promise { this.logger.info('Calling describeClusterTasks'); - // resolve ECS cluster param to value - const clusterRef = await this.getSSMParameter(clusterName); + // resolve ECS cluster param to value + const clusterRef = await this.getSSMParameter(clusterName); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ECSClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: DescribeTasksCommandInput = { - cluster: clusterRef.Parameter?.Value?.toString() || "", + cluster: clusterRef.Parameter?.Value?.toString() ?? '', tasks: taskArns, }; const command = new DescribeTasksCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Update an ECS Service with desire task number. @@ -205,6 +212,8 @@ export class AwsAppsApi { * * @param clusterName - The ECS Cluster name * @param serviceName - The ECS Service name + * @param taskDefinition - The name of the ECS task definition + * @param restart - Enable/disable task restart on new deployment * @param numberOfTasks - The number of tasks desired - use 0 or 1 to reset * @returns The UpdateServiceCommandOutput object * @@ -218,22 +227,26 @@ export class AwsAppsApi { ): Promise { this.logger.info('Calling updateServiceTask'); // resolve ECS cluster param to value - const clusterRef = await this.getSSMParameter(clusterName) + const clusterRef = await this.getSSMParameter(clusterName); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ECSClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: UpdateServiceCommandInput = { - cluster: clusterRef.Parameter?.Value?.toString() || "", + cluster: clusterRef.Parameter?.Value?.toString() ?? '', service: serviceName, desiredCount: numberOfTasks, forceNewDeployment: restart, taskDefinition: taskDefinition, }; const command = new UpdateServiceCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Get SecretsManager Secret value. @@ -241,22 +254,27 @@ export class AwsAppsApi { * @remarks * Get SecretsManager Secret value. * - * @param secretArn - The Arn of the secret to retrive + * @param secretArn - The Arn of the secret to retrieve * @returns The GetSecretValueCommandOutput object * */ public async getSecretValue(secretArn: string): Promise { this.logger.info('Calling getSecretValue'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SecretsManagerClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: GetSecretValueCommandInput = { SecretId: secretArn, }; const command = new GetSecretValueCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** @@ -265,10 +283,16 @@ export class AwsAppsApi { * create SecretsManager Secret. * * @param secretName - The name of the secret + * @param description - The description of the secret + * @param tags - The tags allocated to the secret * @returns The CreateSecretCommandOutput object * */ - public async createSecret(secretName: string, description: string, tags?: { Key: string, Value: string | number | boolean }[]): Promise { + public async createSecret( + secretName: string, + description: string, + tags?: { Key: string; Value: string | number | boolean }[], + ): Promise { this.logger.info('Calling create Secret'); // convert tags to SecretsManager.Tag format @@ -276,9 +300,14 @@ export class AwsAppsApi { return { Key: tag.Key, Value: tag.Value.toString() }; }); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SecretsManagerClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: CreateSecretCommandInput = { Name: secretName, @@ -288,8 +317,7 @@ export class AwsAppsApi { }; const command = new CreateSecretCommand(params); try { - const response = await client.send(command); - return response; + return await client.send(command); } catch (error: any) { throw Error(`Error creating Secret - ${error.toString()}`); } @@ -306,9 +334,15 @@ export class AwsAppsApi { */ public async putSecretValue(secretArn: string, secretValue: string): Promise { this.logger.info('Calling put Secret'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SecretsManagerClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: PutSecretValueCommandInput = { SecretId: secretArn, @@ -316,8 +350,7 @@ export class AwsAppsApi { }; const command = new PutSecretValueCommand(params); try { - const response = await client.send(command); - return response; + return await client.send(command); } catch (error) { throw Error('Error updating secret value'); } @@ -328,10 +361,14 @@ export class AwsAppsApi { * create an S3 bucket. * * @param bucketName - The name of the bucket + * @param tags - tags allocated to the bucket * @returns The CreateBucketCommandOutput object * */ - public async createS3Bucket(bucketName: string, tags?: { Key: string, Value: string | number | boolean }[]): Promise { + public async createS3Bucket( + bucketName: string, + tags?: { Key: string; Value: string | number | boolean }[], + ): Promise { this.logger.info('Calling create S3 bucket'); const fullBucketName = `${bucketName}-${this.awsAccount}-${this.awsRegion}`; @@ -340,9 +377,14 @@ export class AwsAppsApi { return { Key: tag.Key, Value: tag.Value.toString() }; }); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new S3Client({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const createInput: CreateBucketCommandInput = { Bucket: fullBucketName, @@ -361,10 +403,10 @@ export class AwsAppsApi { const response = await client.send(command); const tagInput = { - "Bucket": fullBucketName, - "Tagging": { - "TagSet": resourceTags - } + Bucket: fullBucketName, + Tagging: { + TagSet: resourceTags, + }, }; const tagCommand = new PutBucketTaggingCommand(tagInput); @@ -385,9 +427,14 @@ export class AwsAppsApi { public async doesS3FileExist(bucketName: string, fileName: string): Promise { this.logger.info('Calling doesS3FileExist'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new S3Client({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { @@ -395,9 +442,7 @@ export class AwsAppsApi { Key: fileName, }; const command = new HeadObjectCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** * Get CloudWatch log groups metadata array based on the supplied logPrefix. @@ -411,16 +456,21 @@ export class AwsAppsApi { */ public async getLogGroups(logPrefix: string): Promise { this.logger.info('Calling getLogGroups'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudWatchLogsClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: DescribeLogGroupsCommandInput = { logGroupNamePrefix: logPrefix, }; const command = new DescribeLogGroupsCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Get the metadata of the CloudWatch log group streams. @@ -434,9 +484,15 @@ export class AwsAppsApi { */ public async getLogStreams(logGroupName: string): Promise { this.logger.info('Calling getLogStreams'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudWatchLogsClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: DescribeLogStreamsCommandInput = { logGroupName, @@ -444,8 +500,7 @@ export class AwsAppsApi { descending: true, }; const command = new DescribeLogStreamsCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Get CloudWatch log group streams content. @@ -465,9 +520,15 @@ export class AwsAppsApi { startFromHead = true, ): Promise { this.logger.info('Calling getLogGroupEvents'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudWatchLogsClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: GetLogEventsCommandInput = { logGroupName: logGroupName, @@ -475,29 +536,38 @@ export class AwsAppsApi { startFromHead, }; const command = new GetLogEventsCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } public async getLogRecord(logRecordPointer: string): Promise { this.logger.info('Calling getLogRecord'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudWatchLogsClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: GetLogRecordCommandInput = { logRecordPointer: logRecordPointer, }; const command = new GetLogRecordCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } public async getDynamodbTable(tableName: string, appName: string, timeFrame: number): Promise { this.logger.info('Calling getDynamodbTable'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new DynamoDBClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); console.log(timeFrame); const params: ScanCommandInput = { @@ -509,22 +579,24 @@ export class AwsAppsApi { '#appName': 'appName', }, FilterExpression: '#appName = :appName', - //TODO: Add Query to fetch record by user for a giving time frame , use secondary index for time. need to calculate time backward - // appName -> search critera , timeframe. + // TODO: Add Query to fetch record by user for a giving time frame , use secondary index for time. need to calculate time backward + // appName -> search criteria , timeframe. }; const command = new ScanCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } - - public async putDynamodbTableData(data: DynamoDBTableData): Promise { this.logger.info('Calling getDynamodbTable'); - + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new DynamoDBClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: PutItemCommandInput = { TableName: data.tableName, @@ -533,7 +605,7 @@ export class AwsAppsApi { createdAt: { S: new Date().toISOString() }, createdDate: { S: new Date().toLocaleDateString() }, origin: { S: data.origin }, - appName: {S: data.appName}, + appName: { S: data.appName }, actionType: { S: data.actionType }, actionName: { S: data.name }, initiatedBy: { S: data.initiatedBy }, @@ -549,8 +621,7 @@ export class AwsAppsApi { }, }; const command = new PutItemCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** @@ -566,16 +637,20 @@ export class AwsAppsApi { public async getResourceGroupResources(resourceGroupName: string): Promise { this.logger.info('Calling getResourceGroupResources'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ResourceGroupsClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: ListGroupResourcesCommandInput = { Group: resourceGroupName, }; const command = new ListGroupResourcesCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** @@ -585,14 +660,14 @@ export class AwsAppsApi { * Get resources from a named Resource Group, parses them to destructure the ResourceType, and * categorizes the resources by service identifier * - * @param resourceGroupName - The Resource Group name to get a list of resources from. This can be a string representing an ARN or the name of the Resource Group * @returns A ServiceResources object containing the grouped services + * @param resourceGroup - the name of the resource group */ public async getCategorizedResources(resourceGroup: string): Promise { const rawResults = await this.getResourceGroupResources(resourceGroup); const resourceIdentifiers = rawResults.Resources ?? []; - let categorizedResources = resourceIdentifiers.reduce((acc, item): any => { + return resourceIdentifiers.reduce((acc, item): any => { const idObj = item.Identifier; if (idObj?.ResourceType) { const resourceTypeId = idObj.ResourceType; @@ -602,19 +677,26 @@ export class AwsAppsApi { try { const { resource, service } = parseArn(resourceArn); // Use a regex pattern to extract the resource name without the service resource type - // Most arns begin the resource name after a '/', but some (like CW logs) start after a ':' - const re = /.*?([:?\/])(.*)/; - const reMatches = resource.match(re); + // Most ARNs begin the resource name after a '/', but some (like CW logs) start after a ':' + const re = /.*?([:?/])(.*)/; + const reMatches = RegExp(re).exec(resource); let resourceName = reMatches ? reMatches[2] : resource; // Handle the exception case for SSM Parameters where path-like values need to be prefixed with '/' - if (service == 'ssm' && resource.startsWith('parameter') && resourceName.indexOf('/') > 0) { + if (service === 'ssm' && resource.startsWith('parameter') && resourceName.indexOf('/') > 0) { resourceName = `/${resourceName}`; } if (acc[serviceName]) { acc[serviceName] = [ ...acc[serviceName], - ...[{ resourceTypeId, resourceTypeName, resourceArn, resourceName }], + ...[ + { + resourceTypeId, + resourceTypeName, + resourceArn, + resourceName, + }, + ], ]; } else { acc[serviceName] = [{ resourceTypeId, resourceTypeName, resourceArn, resourceName }]; @@ -623,12 +705,9 @@ export class AwsAppsApi { throw new Error(`Invalid arn provided for ${serviceName} in resource group ${resourceGroup}`); } return acc; - } else { - throw new Error('Could not parse resource group resources response'); } + throw new Error('Could not parse resource group resources response'); }, {}); - - return categorizedResources; } /** @@ -644,9 +723,14 @@ export class AwsAppsApi { public async getSSMParameter(ssmParamName: string): Promise { this.logger.info(`Calling getSSMParameter - ${ssmParamName}`); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SSMClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: GetParameterCommandInput = { @@ -654,8 +738,7 @@ export class AwsAppsApi { WithDecryption: true, }; const command = new GetParameterCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Describe Task definition @@ -667,31 +750,42 @@ export class AwsAppsApi { */ public async describeTaskDefinition(taskDefinitionArn: string): Promise { this.logger.info('Calling Describe Task Definition'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ECSClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: DescribeTaskDefinitionCommandInput = { taskDefinition: taskDefinitionArn, }; const command = new DescribeTaskDefinitionCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** * Register Task Definition * * - * @param TaskDefinition + * @param taskDefinition - TaskDefinition * @returns The DescribeTaskDefinitionCommandOutput object * */ public async registerTaskDefinition(taskDefinition: TaskDefinition): Promise { this.logger.info('Calling Register Task Definition'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new ECSClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const registerTdParams: RegisterTaskDefinitionCommandInput = { containerDefinitions: taskDefinition.containerDefinitions, @@ -706,9 +800,7 @@ export class AwsAppsApi { const registerCommand = new RegisterTaskDefinitionCommand(registerTdParams); - const resp = client.send(registerCommand); - - return resp; + return client.send(registerCommand); } /** @@ -723,17 +815,21 @@ export class AwsAppsApi { */ public async describeStack(stackName: string): Promise { this.logger.info('Calling describeStack'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudFormationClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { StackName: stackName, }; const command = new DescribeStacksCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** @@ -746,22 +842,23 @@ export class AwsAppsApi { * @returns The DescribeStackEventsCommandOutput object * */ - public async describeStackEvents( - stackName: string, - ): Promise { - + public async describeStackEvents(stackName: string): Promise { this.logger.info('Calling describeStackEvents'); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudFormationClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { StackName: stackName, }; const command = new DescribeStackEventsCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** @@ -776,7 +873,7 @@ export class AwsAppsApi { * @param cfFileName - the SAM/CloudFormation template file name * @param providerName - the environment provider name * @param parameters - CloudFormation stack input parameters or undefined - * + * * @returns The UpdateStackCommandOutput object */ public async updateStack( @@ -787,23 +884,23 @@ export class AwsAppsApi { providerName: string, parameters?: Parameter[], ): Promise { - this.logger.info('Calling updateStack'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudFormationClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { StackName: stackName, TemplateURL: `https://${s3BucketName}.s3.amazonaws.com/${cfFileName}`, Parameters: parameters, - Capabilities: [ - Capability.CAPABILITY_IAM, - Capability.CAPABILITY_NAMED_IAM, - Capability.CAPABILITY_AUTO_EXPAND, - ], + Capabilities: [Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM, Capability.CAPABILITY_AUTO_EXPAND], Tags: [ { Key: `aws-apps:${componentName}-${providerName}`, @@ -812,9 +909,7 @@ export class AwsAppsApi { ], }; const command = new UpdateStackCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** @@ -829,7 +924,7 @@ export class AwsAppsApi { * @param cfFileName - the SAM/CloudFormation template file name * @param providerName - the environment provider name * @param parameters - CloudFormation stack input parameters or undefined - * + * * @returns The CreateStackCommandOutput object */ public async createStack( @@ -840,22 +935,23 @@ export class AwsAppsApi { providerName: string, parameters?: Parameter[], ): Promise { - this.logger.info('Calling createStack'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudFormationClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { StackName: stackName, TemplateURL: `https://${s3BucketName}.s3.amazonaws.com/${cfFileName}`, Parameters: parameters, - Capabilities: [ - Capability.CAPABILITY_NAMED_IAM, - Capability.CAPABILITY_AUTO_EXPAND, - ], + Capabilities: [Capability.CAPABILITY_NAMED_IAM, Capability.CAPABILITY_AUTO_EXPAND], Tags: [ { Key: `aws-apps:${componentName}-${providerName}`, @@ -864,9 +960,7 @@ export class AwsAppsApi { ], }; const command = new CreateStackCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** @@ -879,67 +973,75 @@ export class AwsAppsApi { * @returns The DeleteStackCommandOutput object * */ - public async deleteStack( - stackName: string, - ): Promise { - + public async deleteStack(stackName: string): Promise { this.logger.info('Calling deleteStack'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new CloudFormationClient({ region: this.awsRegion, - credentials: this.awsCredentials, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const input = { StackName: stackName, }; const command = new DeleteStackCommand(input); - const response = await client.send(command); - - return response; + return client.send(command); } /** - * Get EKS Cluster - * - * @remarks - * Get information about an EKS Cluster. - * - * @param clusterName - The EKS Cluster name - * @returns The DescribeClusterCommandOutput object - * - */ -public async getEksCluster(clusterName: string): Promise { - this.logger.info('Calling getEksCluster'); - const client = new EKSClient({ - region: this.awsRegion, - credentials: this.awsCredentials, - }); - const params: DescribeClusterCommandInput = { - name: clusterName, - }; - const command = new DescribeClusterCommand(params); - const response = await client.send(command); - return response; -} + * Get EKS Cluster + * + * @remarks + * Get information about an EKS Cluster. + * + * @param clusterName - The EKS Cluster name + * @returns The DescribeClusterCommandOutput object + * + */ + public async getEksCluster(clusterName: string): Promise { + this.logger.info('Calling getEksCluster'); -public async callLambda(functionName: string, body: string) :Promise -{ - this.logger.info('Calling callLambda'); - const client = new LambdaClient({ - region: this.awsRegion, - credentials: this.awsCredentials, - }); - - const params: InvokeCommandInput = { - FunctionName: functionName, - LogType: 'Tail', - Payload: Buffer.from(body), - InvocationType:'RequestResponse' - }; - const command = new InvokeCommand(params); - const response = await client.send(command); - return response; -} + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); + const client = new EKSClient({ + region: this.awsRegion, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, + }); + const params: DescribeClusterCommandInput = { + name: clusterName, + }; + const command = new DescribeClusterCommand(params); + return client.send(command); + } + + public async callLambda(functionName: string, body: string): Promise { + this.logger.info('Calling callLambda'); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); + const client = new LambdaClient({ + region: this.awsRegion, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, + }); + + const params: InvokeCommandInput = { + FunctionName: functionName, + LogType: 'Tail', + Payload: Buffer.from(body), + InvocationType: 'RequestResponse', + }; + const command = new InvokeCommand(params); + return client.send(command); + } } diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-audit.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-audit.ts index f7c30ee4..832f57d8 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-audit.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-audit.ts @@ -4,10 +4,11 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { AwsAppsApi } from './AwsAppsApi'; +/** @public */ export interface AwsAuditRequest { envProviderPrefix: string; envProviderName: string; - appName:string; + appName: string; apiClient: AwsAppsApi; roleArn: string; logger: LoggerService; @@ -22,11 +23,13 @@ export interface AwsAuditRequest { message?: string; } +/** @public */ export interface AwsAuditResponse { status: string; message: string; } +/** @public */ export async function createAuditRecord({ envProviderPrefix, envProviderName, @@ -47,7 +50,9 @@ export async function createAuditRecord({ let tableNameResponse; try { - tableNameResponse = await apiClient.getSSMParameter(`/${envProviderPrefix.toLowerCase()}/${envProviderName.toLowerCase()}/${envProviderName.toLowerCase()}-audit`); + tableNameResponse = await apiClient.getSSMParameter( + `/${envProviderPrefix.toLowerCase()}/${envProviderName.toLowerCase()}/${envProviderName.toLowerCase()}-audit`, + ); } catch (err) { response.status = 'FAILED'; response.message = `Audit failed - audit table name was set to FIXME. ${tableNameResponse}`; @@ -61,7 +66,7 @@ export async function createAuditRecord({ origin: 'Backstage-SDK', prefix: envProviderPrefix, environmentProviderName: envProviderName, - appName:appName, + appName: appName, actionType, name: actionName, initiatedBy: requester, @@ -71,10 +76,10 @@ export async function createAuditRecord({ targetRegion: awsRegion, request: requestArgs ?? '', status, - message: message ?? '' + message: message ?? '', }); - if (auditResponse.$metadata.httpStatusCode == 200) { + if (auditResponse.$metadata.httpStatusCode === 200) { response.status = 'Success'; } else { response.status = 'FAILED'; diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-auth.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-auth.ts index 3aa2a150..3d1980cd 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-auth.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-auth.ts @@ -1,11 +1,12 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 +import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'; import { AwsCredentialIdentity } from '@aws-sdk/types'; -import { getRootLogger } from '@backstage/backend-common'; -import { BackstageUserInfo } from '@backstage/backend-plugin-api'; +import { BackstageUserInfo, LoggerService } from '@backstage/backend-plugin-api'; import { parseEntityRef, UserEntity } from '@backstage/catalog-model'; + +/** @public */ export interface AwsAuthResponse { credentials: AwsCredentialIdentity; requester: string; @@ -27,34 +28,34 @@ function getMemberGroupFromUserEntity(user: UserEntity | undefined) { // if the user has no relations and isn't a member of any groups, then bail early throw new Error('User is not a member of any groups and cannot get mapped AWS credentials'); } - const memberGroups = user.relations.reduce((groups, relation) => { + return user.relations.reduce((groups, relation) => { return parseEntityref(relation.targetRef, groups); }, new Array()); - return memberGroups; } function getMemberGroupFromUserIdentity(user: BackstageUserInfo | undefined) { if (user?.ownershipEntityRefs === undefined) { // if the user has no relations and isn't a member of any groups, then bail early throw new Error('User is not a member of any groups and cannot get mapped AWS credentials'); } - const memberGroups = user.ownershipEntityRefs.reduce((groups, ownershipRefs) => { + return user.ownershipEntityRefs.reduce((groups, ownershipRefs) => { return parseEntityref(ownershipRefs, groups); }, new Array()); - return memberGroups; } async function fetchCreds( + config: Config, + logger: LoggerService, memberGroups: string[], region: string, accountId: string, userName: string, prefix: string, - providerName: string + providerName: string, ): Promise { - const logger = getRootLogger(); try { - // TODO: remove this code once we reference memberGroups - if (memberGroups) { } + if (memberGroups) { + logger.debug(memberGroups.flat().toString()); + } // Get the SSM Parameter pointing to the DynamoDB security mapping table // const ssmClient = new SSMClient({ region }); @@ -94,7 +95,15 @@ async function fetchCreds( } // Assume the mapped role with the STS service and return the credentials logger.debug(`Fetching credentials for mapped role: ${roleArn}`); - const stsClient = new STSClient({ region }); + + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); + const stsClient = new STSClient({ + region, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, + }); const stsResult = await stsClient.send( new AssumeRoleCommand({ RoleArn: roleArn, @@ -118,12 +127,15 @@ async function fetchCreds( // if we weren't able to return credentials, throw an error to be caught by callers throw new Error(`Assuming role ${roleArn} failed to return credentials`); } catch (error) { - logger.error(error); + logger.error(`${error}`); throw error; } } +/** @public */ export async function getAWScreds( + config: Config, + logger: LoggerService, accountId: string, region: string, prefix: string, @@ -131,7 +143,6 @@ export async function getAWScreds( user?: UserEntity, userIdentity?: BackstageUserInfo, ): Promise { - const logger = getRootLogger(); let memberGroups: string[]; if (!/\d{12}/.test(accountId)) { // must be a string of 12 digits @@ -144,45 +155,60 @@ export async function getAWScreds( // !FIXME: Temporary workaround in place to always use the role running the Backstage app to assume the operations role const WORKAROUND = true; if (WORKAROUND) { - return getAWSCredsWorkaround(accountId, region, prefix, providerName, user); - } else { - if (user === undefined && userIdentity !== undefined) { - const userName = parseEntityRef(userIdentity?.userEntityRef).name; - logger.info(`Fetching credentials for user ${userName}`); - memberGroups = getMemberGroupFromUserIdentity(userIdentity); - return fetchCreds(memberGroups, region, accountId, userName, prefix, providerName); - } else { - const userName = user?.metadata.name; - logger.info(`Fetching credentials for user ${userName}`); - memberGroups = getMemberGroupFromUserEntity(user); - return fetchCreds(memberGroups, region, accountId, userName!, prefix, providerName); - } + return getAWSCredsWorkaround(config, logger, accountId, region, prefix, providerName, user); + } + if (user === undefined && userIdentity !== undefined) { + const userName = parseEntityRef(userIdentity?.userEntityRef).name; + logger.info(`Fetching credentials for user ${userName}`); + memberGroups = getMemberGroupFromUserIdentity(userIdentity); + return fetchCreds(config, logger, memberGroups, region, accountId, userName, prefix, providerName); } + const userName = user?.metadata.name; + logger.info(`Fetching credentials for user ${userName}`); + memberGroups = getMemberGroupFromUserEntity(user); + return fetchCreds(config, logger, memberGroups, region, accountId, userName!, prefix, providerName); } -export async function getAWSCredsWorkaround(accountId: string, region: string, prefix: string, providerName: string, user?: UserEntity) { - const client = new STSClient({ region }); - const userName = user?.metadata.name || "unknown"; +export async function getAWSCredsWorkaround( + config: Config, + logger: LoggerService, + accountId: string, + region: string, + prefix: string, + providerName: string, + user?: UserEntity, +) { + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); + const client = new STSClient({ + region, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, + }); + const userName = user?.metadata.name ?? 'unknown'; - //assemble the arn format to the desire destination environment + // assemble the arn format to the desire destination environment const roleArn = `arn:aws:iam::${accountId}:role/${prefix}-${providerName}-operations-role`; - console.log(roleArn) + logger.info(`Assuming role: ${roleArn}`); - const stsResult = await client.send(new AssumeRoleCommand({ - RoleArn: roleArn, - RoleSessionName: `${userName}-backstage-session`, - DurationSeconds: 3600, - })); + const stsResult = await client.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: `${userName}-backstage-session`, + DurationSeconds: 3600, + }), + ); return { roleArn, requester: userName, credentials: { - accessKeyId: stsResult!.Credentials!.AccessKeyId!, - secretAccessKey: stsResult!.Credentials!.SecretAccessKey!, - sessionToken: stsResult!.Credentials!.SessionToken, + accessKeyId: stsResult.Credentials!.AccessKeyId!, + secretAccessKey: stsResult.Credentials!.SecretAccessKey!, + sessionToken: stsResult.Credentials!.SessionToken, }, account: accountId, region: region, - } + }; } diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-platform.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-platform.ts index e613d607..764a2c5f 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/aws-platform.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/aws-platform.ts @@ -17,17 +17,19 @@ import { SSMClient, } from '@aws-sdk/client-ssm'; import { - AWSEnvironmentProviderRecord, AppPromoParams, + AWSEnvironmentProviderRecord, BindResourceParams, - GitProviders, - ICommitChange, - IGitAPIResult, - IRepositoryInfo + GitProviders, + ICommitChange, + IGitAPIResult, + IRepositoryInfo, } from '@aws/plugin-aws-apps-common-for-backstage'; import YAML from 'yaml'; import { GitAPI } from './git-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; export type GitLabDownloadFileResponse = { file_name: string; @@ -45,13 +47,14 @@ export type GitLabDownloadFileResponse = { export class AwsAppsPlatformApi { public git: GitAPI; - + public constructor( + private readonly config: Config, private readonly logger: LoggerService, private readonly platformRegion: string, private readonly awsRegion: string, private readonly awsAccount: string, - private readonly gitProvider: GitProviders + private readonly gitProvider: GitProviders, ) { this.logger.info('Instantiating AWS Apps Platform API with:'); this.logger.info(`platformRegion: ${this.platformRegion}`); @@ -74,15 +77,21 @@ export class AwsAppsPlatformApi { public async getPlatformSecretValue(secretArn: string): Promise { this.logger.info(`Calling getPlatformSecretValue for ${secretArn} against platform region ${this.platformRegion}`); + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SecretsManagerClient({ region: this.platformRegion, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); + const params: GetSecretValueCommandInput = { SecretId: secretArn, }; const command = new GetSecretValueCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } /** @@ -97,8 +106,15 @@ export class AwsAppsPlatformApi { */ public async getSsmValue(ssmKey: string): Promise { this.logger.info(`Calling getSsmValue for ${ssmKey} against platform region ${this.platformRegion}`); + + const accountId = this.awsAccount; + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(this.config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({ + accountId, + }); const client = new SSMClient({ region: this.platformRegion, + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const params: GetParameterCommandInput = { @@ -106,8 +122,7 @@ export class AwsAppsPlatformApi { WithDecryption: true, }; const command = new GetParameterCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } public async deletePlatformSecret(secretName: string): Promise { @@ -121,11 +136,9 @@ export class AwsAppsPlatformApi { ForceDeleteWithoutRecovery: true, }; const command = new DeleteSecretCommand(params); - const resp = client.send(command); - return resp; + return client.send(command); } - public async deleteTFProvider( envName: string, providerName: string, @@ -137,16 +150,13 @@ export class AwsAppsPlatformApi { const tfDeleteContent = `PROVIDER_FILE_TO_DELETE=${envName}-${providerName}.properties\nENV_ENTITY_REF="awsenvironment:default/${envName}"\nTARGET_ENV_NAME=${envName}\nTARGET_ENV_PROVIDER_NAME=${providerName}`; let tfDeleteFile; - if (envName==="") - { + if (envName === '') { tfDeleteFile = `env-destroy-params-temp.properties`; - } - else - { - tfDeleteFile = `.awsdeployment/env-destroy-params-temp.properties`; + } else { + tfDeleteFile = `.awsdeployment/env-destroy-params-temp.properties`; } - const change:ICommitChange = { + const change: ICommitChange = { commitMessage: `Destroy TF Infrastructure`, branch: 'main', actions: [ @@ -154,33 +164,29 @@ export class AwsAppsPlatformApi { action: 'create', file_path: tfDeleteFile, content: tfDeleteContent, - } - ] - } + }, + ], + }; const result = await this.git.getGitProvider().commitContent(change, repo, gitToken); - if (!result.isSuccuess) { + if (!result.isSuccess) { console.error(`ERROR: Failed to Destroy ${envName}. Response: ${result}`); - let message = ''; + let message: string; if (result.value?.includes('A file with this name already exists')) { message = `${envName} has already been scheduled for destruction. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`; } else { message = result.value || ''; } return { status: 'FAILURE', message }; - } else { - return { - status: 'SUCCESS', - message: `Destroy will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, - }; } + return { + status: 'SUCCESS', + message: `Destroy will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, + }; } - public async deleteRepository( - repo: IRepositoryInfo, - gitSecretName: string, - ): Promise { + public async deleteRepository(repo: IRepositoryInfo, gitSecretName: string): Promise { const gitToken = await this.getGitToken(gitSecretName); const result = await this.git.getGitProvider().deleteRepository(repo, gitToken); @@ -190,24 +196,18 @@ export class AwsAppsPlatformApi { private async getGitToken(gitSecretName: string): Promise { const gitAdminSecret = await this.getPlatformSecretValue(gitSecretName); - const gitAdminSecretObj = JSON.parse(gitAdminSecret.SecretString || ''); - return gitAdminSecretObj['apiToken']; + const gitAdminSecretObj = JSON.parse(gitAdminSecret.SecretString ?? ''); + return gitAdminSecretObj.apiToken; } - public async getFileContentsFromGit( - repo: IRepositoryInfo, - filePath: string, - gitSecretName: string, - ): Promise { + public async getFileContentsFromGit(repo: IRepositoryInfo, filePath: string, gitSecretName: string): Promise { const gitToken = await this.getGitToken(gitSecretName); - const result = await this.git.getGitProvider().getFileContent(filePath,repo,gitToken); - + const result = await this.git.getGitProvider().getFileContent(filePath, repo, gitToken); + const resultBody = await result.value; - if (!result.isSuccuess) { - console.error( - `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response: ${result}`, - ); + if (!result.isSuccess) { + console.error(`ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response: ${result}`); throw new Error(`Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response: ${result}`); } else { return resultBody; @@ -245,34 +245,29 @@ export class AwsAppsPlatformApi { }), ); - - const change:ICommitChange = { + const change: ICommitChange = { commitMessage: `generate CICD stages`, branch: 'main', - actions - } + actions, + }; const result = await this.git.getGitProvider().commitContent(change, repo, gitToken); - const resultBody = result.value; - if (!result.isSuccuess) { - console.error( - `ERROR: Failed to schedule deployment for ${input.envName}. Response: ${result}`, - ); - let message = ''; + if (!result.isSuccess) { + console.error(`ERROR: Failed to schedule deployment for ${input.envName}. Response: ${result}`); + let message: string; if (resultBody.message?.includes('A file with this name already exists')) { message = `${input.envName} has already been scheduled for deployment. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`; } else { message = resultBody.message || ''; } return { status: 'FAILURE', message }; - } else { - return { - status: 'SUCCESS', - message: `The app will not be ready to run until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, - }; } + return { + status: 'SUCCESS', + message: `The app will not be ready to run until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, + }; } public async bindResource( @@ -301,29 +296,28 @@ export class AwsAppsPlatformApi { content: resourceBindContent, }); - const change:ICommitChange = { + const change: ICommitChange = { commitMessage: `Bind Resource`, branch: 'main', - actions - } + actions, + }; const result = await this.git.getGitProvider().commitContent(change, repo, gitToken); const resultBody = result.value; - if (!result.isSuccuess) { + if (!result.isSuccess) { console.error(`ERROR: Failed to bind ${input.envName}. Response: ${result}`); - let message = ''; + let message: string; if (resultBody.message?.includes('A file with this name already exists')) { message = `${input.envName} has already been scheduled for binding. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`; } else { message = resultBody.message || ''; } return { status: 'FAILURE', message }; - } else { - return { - status: 'SUCCESS', - message: `Binding will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, - }; } + return { + status: 'SUCCESS', + message: `Binding will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, + }; } public async unBindResource( @@ -352,30 +346,29 @@ export class AwsAppsPlatformApi { content: resourceBindContent, }); - const change:ICommitChange = { + const change: ICommitChange = { commitMessage: `UnBind Resource`, branch: 'main', - actions - } + actions, + }; const result = await this.git.getGitProvider().commitContent(change, repo, gitToken); const resultBody = await result.value.json(); - if (!result.isSuccuess) { + if (!result.isSuccess) { console.error(`ERROR: Failed to unbind ${input.envName}. Response: ${result}`); - let message = ''; + let message: string; if (resultBody.message?.includes('A file with this name already exists')) { message = `${input.envName} has already been scheduled for unbinding. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`; } else { message = resultBody.message || ''; } return { status: 'FAILURE', message }; - } else { - return { - status: 'SUCCESS', - message: `Unbinding will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, - }; } + return { + status: 'SUCCESS', + message: `Unbinding will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, + }; } public async updateProvider( @@ -388,7 +381,7 @@ export class AwsAppsPlatformApi { ): Promise<{ status: string; message?: string }> { const gitToken = await this.getGitToken(gitSecretName); - let actions = []; + const actions = []; if (action === 'add') { console.log(entityCatalog); const newDependencies = entityCatalog.spec.dependsOn as Array; @@ -405,10 +398,10 @@ export class AwsAppsPlatformApi { } else if (action === 'remove') { console.log(entityCatalog); const dependencies = entityCatalog.spec.dependsOn as Array; - let newDependencies = Array(); + const newDependencies = Array(); dependencies.forEach(p => { const providerToRemove = `awsenvironmentprovider:default/${provider.name.toLowerCase()}`; - if (p != providerToRemove) { + if (p !== providerToRemove) { newDependencies.push(p); } }); @@ -425,43 +418,35 @@ export class AwsAppsPlatformApi { throw new Error('Not yet implemented'); } - const change:ICommitChange = { + const change: ICommitChange = { commitMessage: `Update Environment Provider`, branch: 'main', - actions - } + actions, + }; const result = await this.git.getGitProvider().commitContent(change, repo, gitToken); - console.log(result) + console.log(result); let resultBody; - if (this.gitProvider===GitProviders.GITLAB) - { - resultBody = await result.value.json(); - } - else if (this.gitProvider===GitProviders.GITHUB) - { - resultBody = result.message - } - - - - if (!result.isSuccuess) { - console.error( - `ERROR: Failed to Update provider ${provider.name}. Response: ${result}`, - ); - let message = ''; + if (this.gitProvider === GitProviders.GITLAB) { + resultBody = await result.value.json(); + } else if (this.gitProvider === GitProviders.GITHUB) { + resultBody = result.message; + } + + if (!result.isSuccess) { + console.error(`ERROR: Failed to Update provider ${provider.name}. Response: ${result}`); + let message: string; if (resultBody.message?.includes('A file with this name already exists')) { message = `Update ${provider.name} has already been scheduled. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`; } else { message = resultBody.message || ''; } return { status: 'FAILURE', message }; - } else { - return { - status: 'SUCCESS', - message: `Update Provider for ${envName} will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, - }; } + return { + status: 'SUCCESS', + message: `Update Provider for ${envName} will not be complete until deployment succeeds. Check the CICD pipeline for the most up-to-date information. UI status may take a few minutes to update.`, + }; } } diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/git-api.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/git-api.ts index 29693462..347fb60d 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/git-api.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/git-api.ts @@ -3,38 +3,30 @@ import { GitLabAPI } from './gitlab-api'; import { GitHubAPI } from './github-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +export class GitAPI { + // The main reference implementation of git + private git: ISCMBackendAPI; -export class GitAPI -{ - // The main reference implementation of git - private git: ISCMBackendAPI - - public constructor( - readonly logger: LoggerService, - readonly gitProvider: GitProviders - ) { - this.logger = logger; - this.gitProvider = gitProvider; - this.logger.info(`Instantiating GitAPI with ${gitProvider}...`); - if (gitProvider === GitProviders.GITLAB) { - this.git = new GitLabAPI(logger); - }else if (gitProvider === GitProviders.GITHUB) { - this.git = new GitHubAPI(logger); - } - else if (gitProvider === GitProviders.UNSET) { - this.git = new GitHubAPI(logger); - } - else - { - throw new Error("Invalid / unsupported Git Provider"); - } - } - - public getGitProviderType():string { - return this.gitProvider - } - - public getGitProvider():ISCMBackendAPI { - return this.git + public constructor(readonly logger: LoggerService, readonly gitProvider: GitProviders) { + this.logger = logger; + this.gitProvider = gitProvider; + this.logger.info(`Instantiating GitAPI with ${gitProvider}...`); + if (gitProvider === GitProviders.GITLAB) { + this.git = new GitLabAPI(logger); + } else if (gitProvider === GitProviders.GITHUB) { + this.git = new GitHubAPI(logger); + } else if (gitProvider === GitProviders.UNSET) { + this.git = new GitHubAPI(logger); + } else { + throw new Error('Invalid / unsupported Git Provider'); } -} \ No newline at end of file + } + + public getGitProviderType(): string { + return this.gitProvider; + } + + public getGitProvider(): ISCMBackendAPI { + return this.git; + } +} diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/github-api.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/github-api.ts index 41e15980..4303e3b4 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/github-api.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/github-api.ts @@ -1,163 +1,165 @@ -import { IGitAPIResult, ICommitChange, IRepositoryInfo, ISCMBackendAPI } from "@aws/plugin-aws-apps-common-for-backstage/src/types/SCMBackendAPI"; -import { Octokit } from "octokit"; -import { Buffer } from "buffer"; -import { LoggerService } from "@backstage/backend-plugin-api"; - +import { + IGitAPIResult, + ICommitChange, + IRepositoryInfo, + ISCMBackendAPI, +} from '@aws/plugin-aws-apps-common-for-backstage'; +import { Octokit } from 'octokit'; +import { Buffer } from 'buffer'; +import { LoggerService } from '@backstage/backend-plugin-api'; export class GitHubAPI implements ISCMBackendAPI { - public constructor( - private readonly logger: LoggerService - ) { - this.logger.info('Instantiating GitHubAPI...'); - } - - public async createRepository(repo: IRepositoryInfo, accessToken: string): Promise { - const octokit = new Octokit({ auth: accessToken }); - const url = `https://${repo.gitHost}`; - console.log(url); - - const createRepoResults = await octokit.rest.repos.createInOrg({ - org: repo.gitOrganization || "", - name:repo.gitRepoName, - description: repo.description || "", - private: repo.isPrivate, - visibility: repo.visibility || "private", - auto_init: true - }); - - console.log(createRepoResults); - if (createRepoResults.status > 299) { - this.logger.error('ERROR: Repository failed to create'); - return { - isSuccuess: false, - message: `Repository failed to create`, - httpResponse : createRepoResults.status, - value:'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Repository created successfully`, - httpResponse : createRepoResults.status, - value:'SUCCESS' - }; - } + public constructor(private readonly logger: LoggerService) { + this.logger.info('Instantiating GitHubAPI...'); + } + + public async createRepository(repo: IRepositoryInfo, accessToken: string): Promise { + const octokit = new Octokit({ auth: accessToken }); + const url = `https://${repo.gitHost}`; + console.log(url); + + const createRepoResults = await octokit.rest.repos.createInOrg({ + org: repo.gitOrganization || '', + name: repo.gitRepoName, + description: repo.description || '', + private: repo.isPrivate, + visibility: repo.visibility || 'private', + auto_init: true, + }); + + console.log(createRepoResults); + if (createRepoResults.status > 299) { + this.logger.error('ERROR: Repository failed to create'); + return { + isSuccess: false, + message: `Repository failed to create`, + httpResponse: createRepoResults.status, + value: 'FAILURE', + }; } + return { + isSuccess: true, + message: `Repository created successfully`, + httpResponse: createRepoResults.status, + value: 'SUCCESS', + }; + } - public async deleteRepository(repo: IRepositoryInfo, accessToken: string) : Promise { - const octokit = new Octokit({ auth: accessToken }); - const url = `https://${repo.gitHost}`; - console.log(url); - - const deleteRepoResults = await octokit.rest.repos.delete({ - owner: repo.gitOrganization || "", - repo:repo.gitRepoName, - }); - - console.log(deleteRepoResults); - if (deleteRepoResults.status > 299) { - this.logger.error('ERROR: Repository failed to delete'); - return { - isSuccuess: false, - message: `Repository failed to delete`, - httpResponse : deleteRepoResults.status, - value:'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Repository deleted successfully`, - httpResponse : deleteRepoResults.status, - value:'SUCCESS' - }; - } + public async deleteRepository(repo: IRepositoryInfo, accessToken: string): Promise { + const octokit = new Octokit({ auth: accessToken }); + const url = `https://${repo.gitHost}`; + console.log(url); + + const deleteRepoResults = await octokit.rest.repos.delete({ + owner: repo.gitOrganization || '', + repo: repo.gitRepoName, + }); + + console.log(deleteRepoResults); + if (deleteRepoResults.status > 299) { + this.logger.error('ERROR: Repository failed to delete'); + return { + isSuccess: false, + message: `Repository failed to delete`, + httpResponse: deleteRepoResults.status, + value: 'FAILURE', + }; } + return { + isSuccess: true, + message: `Repository deleted successfully`, + httpResponse: deleteRepoResults.status, + value: 'SUCCESS', + }; + } + + public async getFileContent(filePath: string, repo: IRepositoryInfo, accessToken: string): Promise { + const octokit = new Octokit({ auth: accessToken }); + // console.log(repo.gitRepoName) + // console.log(repo.gitOrganization) + console.log(filePath); + const result = await octokit.rest.repos.getContent({ + owner: repo.gitOrganization || '', + repo: repo.gitRepoName, + path: filePath, + }); - public async getFileContent(filePath: string, repo: IRepositoryInfo, accessToken: string): Promise { - const octokit = new Octokit({ auth: accessToken }); - // console.log(repo.gitRepoName) - // console.log(repo.gitOrganization) - console.log(filePath) - const result = await octokit.rest.repos.getContent({ - owner: repo.gitOrganization || "", - repo:repo.gitRepoName, - path: filePath - }); - - if (result.status > 299) { - this.logger.error(`ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${result}`); - return { - isSuccuess: false, - message: `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${result}`, - httpResponse : result.status, - value:'FAILURE' - } - } else { - // console.log(result.data) - // console.log(result.data.content) - // const fileDataProcessed = Buffer.from(result.data.content, 'base64').toString('binary') - const dataContent = result.data; - //console.log(dataContent) - const contentB64 = dataContent as any; //cast internal type base 64 string - //console.log(contentB64.content) - const content = Buffer.from(contentB64.content, 'base64').toString() - - return { - isSuccuess: true, - message: `Retrieve file content successfully`, - httpResponse : result.status, - value:content - }; - } + if (result.status > 299) { + this.logger.error( + `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${result}`, + ); + return { + isSuccess: false, + message: `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${result}`, + httpResponse: result.status, + value: 'FAILURE', + }; } + // console.log(result.data) + // console.log(result.data.content) + // const fileDataProcessed = Buffer.from(result.data.content, 'base64').toString('binary') + const dataContent = result.data; + // console.log(dataContent) + const contentB64 = dataContent as any; // cast internal type base 64 string + // console.log(contentB64.content) + const content = Buffer.from(contentB64.content, 'base64').toString(); + + return { + isSuccess: true, + message: `Retrieve file content successfully`, + httpResponse: result.status, + value: content, + }; + } + + public async commitContent( + change: ICommitChange, + repo: IRepositoryInfo, + accessToken: string, + ): Promise { + const OctokitCommitMultipleFiles = Octokit.plugin(require('octokit-commit-multiple-files')); + const octokit = new OctokitCommitMultipleFiles({ auth: accessToken }); - public async commitContent(change: ICommitChange, repo: IRepositoryInfo, accessToken: string): Promise { - const octokitPlugin = Octokit.plugin(require("octokit-commit-multiple-files")); - const octokit = new octokitPlugin({ auth: accessToken }); - - const url = `https://${repo.gitHost}`; - console.log(url); - try { - // supported actions = "deletions" , "additions" - // prepare content - - const changes = change.actions.map( item=> { - return { - message: change.commitMessage, - files: { - [item.file_path] : Buffer.from(item.content).toString('base64'), - } - } - }) - - - const result = await octokit.createOrUpdateFiles({ - owner: repo.gitOrganization || "", - repo:repo.gitRepoName, - branch : change.branch, - createBranch: false, - changes - }); - - - console.log(result) - - return { - isSuccuess: true, - message: `Commit submitted successfully`, - httpResponse : 200, - value:result + const url = `https://${repo.gitHost}`; + console.log(url); + try { + // supported actions = "deletions" , "additions" + // prepare content + + const changes = change.actions.map(item => { + return { + message: change.commitMessage, + files: { + [item.file_path]: Buffer.from(item.content).toString('base64'), + }, }; + }); + + const result = await octokit.createOrUpdateFiles({ + owner: repo.gitOrganization || '', + repo: repo.gitRepoName, + branch: change.branch, + createBranch: false, + changes, + }); + + console.log(result); - } catch (error) { - this.logger.error(`ERROR: Failed to submit commit to ${repo.gitRepoName}`); - this.logger.error(JSON.stringify(error)); - return { - isSuccuess: false, - message: `ERROR: Failed to submit commit to ${repo.gitRepoName}`, - httpResponse : 500, - value:error || 'FAILURE' - } - } + return { + isSuccess: true, + message: `Commit submitted successfully`, + httpResponse: 200, + value: result, + }; + } catch (error) { + this.logger.error(`ERROR: Failed to submit commit to ${repo.gitRepoName}`); + this.logger.error(JSON.stringify(error)); + return { + isSuccess: false, + message: `ERROR: Failed to submit commit to ${repo.gitRepoName}`, + httpResponse: 500, + value: error || 'FAILURE', + }; } -} \ No newline at end of file + } +} diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/gitlab-api.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/gitlab-api.ts index 98031283..b2a009ea 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/gitlab-api.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/gitlab-api.ts @@ -1,210 +1,218 @@ -import { IGitAPIResult, ICommitChange, IRepositoryInfo, ISCMBackendAPI } from "@aws/plugin-aws-apps-common-for-backstage/src/types/SCMBackendAPI"; -import { LoggerService } from "@backstage/backend-plugin-api"; +import { + IGitAPIResult, + ICommitChange, + IRepositoryInfo, + ISCMBackendAPI, +} from '@aws/plugin-aws-apps-common-for-backstage'; +import { LoggerService } from '@backstage/backend-plugin-api'; export class GitLabAPI implements ISCMBackendAPI { - - public constructor( - private readonly logger: LoggerService - ) { - this.logger.info('Instantiating GitLabAPI...'); - } - - private async getGitProjectId( - gitHost: string, - gitProjectGroup: string, - gitRepoName: string, - accessToken: string, - ): Promise { - let repoName; - let groupName; - - if (gitRepoName.includes('/')) { - groupName = gitRepoName.split('/')[0] - repoName = gitRepoName.split('/')[1] - } - else - { - groupName="" - repoName = gitRepoName; - } - const url = `https://${gitHost}/api/v4/projects?search=${repoName}`; - const gitProjects = await fetch(url, { - method: 'GET', - headers: { - 'PRIVATE-TOKEN': accessToken, - 'Content-Type': 'application/json', - }, - }); - const gitProjectsJson: { path_with_namespace: string; id: string }[] = await gitProjects.json(); - // console.log(gitProjectsJson) - let project = null; - if (gitProjectsJson) { - project = gitProjectsJson.filter( - project => project.path_with_namespace === `${groupName}/${repoName}`, - )[0]; - } - - if (project && project.id) { - return project.id; - } else { - throw new Error(`Failed to get git project ID for group '${gitProjectGroup}' and repo '${gitRepoName}'`); - } - } - - public async createRepository(repo: IRepositoryInfo, accessToken: string): Promise { - // THIS IS NOT TESTED YET - - const url = `https://${repo.gitHost}/api/v4/projects/`; - const body = { - "name": repo.gitRepoName, - "description": repo.description || "", - "path": repo.gitProjectGroup? (repo.gitProjectGroup + "/" + repo.gitRepoName) : repo.gitRepoName, - "initialize_with_readme": "true" - } - - console.log(url); - const createRepoResults = await fetch(url, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'PRIVATE-TOKEN': accessToken, - }, - }); - console.log(createRepoResults); - if (createRepoResults.status > 299) { - this.logger.error('ERROR: Repository failed to create'); - return { - isSuccuess: false, - message: `Repository failed to create`, - httpResponse : createRepoResults.status, - value:'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Repository created successfully`, - httpResponse : createRepoResults.status, - value:'SUCCESS' - }; - } + public constructor(private readonly logger: LoggerService) { + this.logger.info('Instantiating GitLabAPI...'); + } + + private async getGitProjectId( + gitHost: string, + gitProjectGroup: string, + gitRepoName: string, + accessToken: string, + ): Promise { + let repoName: string; + let groupName: string; + + if (gitRepoName.includes('/')) { + groupName = gitRepoName.split('/')[0]; + repoName = gitRepoName.split('/')[1]; + } else { + groupName = ''; + repoName = gitRepoName; } - - public async deleteRepository(repo: IRepositoryInfo, accessToken: string) : Promise { - let gitProjectId: string = ""; - if (!repo.projectID) - { - gitProjectId = await this.getGitProjectId(repo.gitHost, repo.gitProjectGroup || "", repo.gitRepoName, accessToken); - console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); - } - - // now delete the repo - const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}`; - console.log(url); - const deleteRepoResults = await fetch(url, { - method: 'DELETE', - headers: { - 'PRIVATE-TOKEN': accessToken, - }, - }); - console.log(deleteRepoResults); - if (deleteRepoResults.status > 299) { - this.logger.error('ERROR: Repository failed to delete'); - return { - isSuccuess: false, - message: `Repository failed to delete`, - httpResponse : deleteRepoResults.status, - value:'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Repository deleted successfully`, - httpResponse : deleteRepoResults.status, - value:'SUCCESS' - }; - } + const url = `https://${gitHost}/api/v4/projects?search=${repoName}`; + const gitProjects = await fetch(url, { + method: 'GET', + headers: { + 'PRIVATE-TOKEN': accessToken, + 'Content-Type': 'application/json', + }, + }); + const gitProjectsJson: { path_with_namespace: string; id: string }[] = await gitProjects.json(); + // console.log(gitProjectsJson) + let project = null; + if (gitProjectsJson) { + project = gitProjectsJson.filter(p => p.path_with_namespace === `${groupName}/${repoName}`)[0]; } - public async getFileContent(filePath: string, repo: IRepositoryInfo, accessToken: string): Promise { - let gitProjectId: string = ""; - if (!repo.projectID) - { - gitProjectId = await this.getGitProjectId(repo.gitHost, repo.gitProjectGroup || "", repo.gitRepoName, accessToken); - console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); - } - - const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}/repository/files/${filePath}?ref=main`; - const result = await fetch(new URL(url), { - method: 'GET', - headers: { - 'PRIVATE-TOKEN': accessToken, - 'Content-Type': 'application/json', - }, - }); - - const resultBody = await result.json(); - if (result.status > 299) { - this.logger.error(`ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${resultBody}`); - return { - isSuccuess: false, - message: `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${resultBody}`, - httpResponse : result.status, - value:'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Retrieved file content successfully`, - httpResponse : result.status, - value:resultBody.content - }; - } + if (project && project.id) { + return project.id; + } + throw new Error(`Failed to get git project ID for group '${gitProjectGroup}' and repo '${gitRepoName}'`); + } + + public async createRepository(repo: IRepositoryInfo, accessToken: string): Promise { + // THIS IS NOT TESTED YET + + const url = `https://${repo.gitHost}/api/v4/projects/`; + const body = { + name: repo.gitRepoName, + description: repo.description || '', + path: repo.gitProjectGroup ? `${repo.gitProjectGroup}/${repo.gitRepoName}` : repo.gitRepoName, + initialize_with_readme: 'true', + }; + + console.log(url); + const createRepoResults = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'PRIVATE-TOKEN': accessToken, + }, + }); + console.log(createRepoResults); + if (createRepoResults.status > 299) { + this.logger.error('ERROR: Repository failed to create'); + return { + isSuccess: false, + message: `Repository failed to create`, + httpResponse: createRepoResults.status, + value: 'FAILURE', + }; + } + return { + isSuccess: true, + message: `Repository created successfully`, + httpResponse: createRepoResults.status, + value: 'SUCCESS', + }; + } + + public async deleteRepository(repo: IRepositoryInfo, accessToken: string): Promise { + let gitProjectId: string = ''; + if (!repo.projectID) { + gitProjectId = await this.getGitProjectId( + repo.gitHost, + repo.gitProjectGroup || '', + repo.gitRepoName, + accessToken, + ); + console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); } - public async commitContent(change: ICommitChange, repo: IRepositoryInfo, accessToken: string): Promise { - let gitProjectId: string = ""; - if (!repo.projectID) - { - gitProjectId = await this.getGitProjectId(repo.gitHost, repo.gitProjectGroup || "", repo.gitRepoName, accessToken); - console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); - } - - const commit = { - branch: change.branch, - commit_message: change.commitMessage, - actions: change.actions, - }; - - const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}/repository/commits`; - const result = await fetch(new URL(url), { - method: 'POST', - body: JSON.stringify(commit), - headers: { - 'PRIVATE-TOKEN': accessToken, - 'Content-Type': 'application/json', - }, - }); - - const resultBody = await result.json(); - - if (result.status > 299) { - this.logger.error(`ERROR: Failed to submit commit to ${repo.gitRepoName}`); - return { - isSuccuess: false, - message: `ERROR: Failed to submit commit to ${repo.gitRepoName}`, - httpResponse : result.status, - value:resultBody.message || 'FAILURE' - } - } else { - return { - isSuccuess: true, - message: `Commit submitted successfully`, - httpResponse : result.status, - value:resultBody - }; - } + // now delete the repo + const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}`; + console.log(url); + const deleteRepoResults = await fetch(url, { + method: 'DELETE', + headers: { + 'PRIVATE-TOKEN': accessToken, + }, + }); + console.log(deleteRepoResults); + if (deleteRepoResults.status > 299) { + this.logger.error('ERROR: Repository failed to delete'); + return { + isSuccess: false, + message: `Repository failed to delete`, + httpResponse: deleteRepoResults.status, + value: 'FAILURE', + }; + } + return { + isSuccess: true, + message: `Repository deleted successfully`, + httpResponse: deleteRepoResults.status, + value: 'SUCCESS', + }; + } + + public async getFileContent(filePath: string, repo: IRepositoryInfo, accessToken: string): Promise { + let gitProjectId: string = ''; + if (!repo.projectID) { + gitProjectId = await this.getGitProjectId( + repo.gitHost, + repo.gitProjectGroup || '', + repo.gitRepoName, + accessToken, + ); + console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); } - + const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}/repository/files/${filePath}?ref=main`; + const result = await fetch(new URL(url), { + method: 'GET', + headers: { + 'PRIVATE-TOKEN': accessToken, + 'Content-Type': 'application/json', + }, + }); + + const resultBody = await result.json(); + if (result.status > 299) { + this.logger.error( + `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${resultBody}`, + ); + return { + isSuccess: false, + message: `ERROR: Failed to retrieve ${filePath} for ${repo.gitRepoName}. Response code: ${result.status} - ${resultBody}`, + httpResponse: result.status, + value: 'FAILURE', + }; + } + return { + isSuccess: true, + message: `Retrieved file content successfully`, + httpResponse: result.status, + value: resultBody.content, + }; + } + + public async commitContent( + change: ICommitChange, + repo: IRepositoryInfo, + accessToken: string, + ): Promise { + let gitProjectId: string = ''; + if (!repo.projectID) { + gitProjectId = await this.getGitProjectId( + repo.gitHost, + repo.gitProjectGroup || '', + repo.gitRepoName, + accessToken, + ); + console.log(`Got GitLab project ID: ${gitProjectId} for ${repo.gitProjectGroup}/${repo.gitRepoName}`); + } + const commit = { + branch: change.branch, + commit_message: change.commitMessage, + actions: change.actions, + }; + + const url = `https://${repo.gitHost}/api/v4/projects/${gitProjectId}/repository/commits`; + const result = await fetch(new URL(url), { + method: 'POST', + body: JSON.stringify(commit), + headers: { + 'PRIVATE-TOKEN': accessToken, + 'Content-Type': 'application/json', + }, + }); + + const resultBody = await result.json(); + + if (result.status > 299) { + this.logger.error(`ERROR: Failed to submit commit to ${repo.gitRepoName}`); + return { + isSuccess: false, + message: `ERROR: Failed to submit commit to ${repo.gitRepoName}`, + httpResponse: result.status, + value: resultBody.message || 'FAILURE', + }; + } + return { + isSuccess: true, + message: `Commit submitted successfully`, + httpResponse: result.status, + value: resultBody, + }; + } } diff --git a/backstage-plugins/plugins/aws-apps-backend/src/api/index.ts b/backstage-plugins/plugins/aws-apps-backend/src/api/index.ts index d8d3a184..57591d24 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/api/index.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/api/index.ts @@ -3,4 +3,6 @@ export { AwsAppsApi } from './AwsAppsApi'; export { getAWScreds, type AwsAuthResponse } from './aws-auth'; -export { createAuditRecord } from './aws-audit'; \ No newline at end of file +export { createAuditRecord } from './aws-audit'; +export type { DynamoDBTableData } from './AwsAppsApi'; +export type { AwsAuditRequest, AwsAuditResponse } from './aws-audit'; diff --git a/backstage-plugins/plugins/aws-apps-backend/src/index.ts b/backstage-plugins/plugins/aws-apps-backend/src/index.ts index 6cbecd4e..47f7a424 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/index.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/index.ts @@ -4,5 +4,4 @@ export { awsAppsPlugin as default } from './plugin'; export * from './api'; export * from './service/router'; - - +export type { DynamoDBTableData } from './api'; diff --git a/backstage-plugins/plugins/aws-apps-backend/src/plugin.ts b/backstage-plugins/plugins/aws-apps-backend/src/plugin.ts index f38d229b..26ae95e4 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/plugin.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/plugin.ts @@ -1,9 +1,7 @@ -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; +import { BackendPluginRegistrationPoints, coreServices, createBackendPlugin } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; + /** * awsAppsPlugin backend plugin * @@ -11,7 +9,7 @@ import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; */ export const awsAppsPlugin = createBackendPlugin({ pluginId: 'aws-apps-backend', - register(env) { + register(env: BackendPluginRegistrationPoints): void { env.registerInit({ deps: { config: coreServices.rootConfig, @@ -23,16 +21,7 @@ export const awsAppsPlugin = createBackendPlugin({ auth: coreServices.auth, httpAuth: coreServices.httpAuth, }, - async init({ - config, - logger, - httpRouter, - userInfo, - catalogApi, - permissions, - auth, - httpAuth, - }) { + async init({ config, logger, httpRouter, userInfo, catalogApi, permissions, auth, httpAuth }) { httpRouter.use( await createRouter({ config, diff --git a/backstage-plugins/plugins/aws-apps-backend/src/service/router.test.ts b/backstage-plugins/plugins/aws-apps-backend/src/service/router.test.ts index a9f5eae1..58880b30 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/service/router.test.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/service/router.test.ts @@ -1,32 +1,40 @@ -// import { mockServices } from '@backstage/backend-test-utils'; -// import express from 'express'; -// import request from 'supertest'; +import { mockServices } from '@backstage/backend-test-utils'; +import { CatalogApi } from '@backstage/catalog-client'; +import express from 'express'; +import request from 'supertest'; -// import { createRouter } from './router'; +import { createRouter } from './router'; -// describe('createRouter', () => { -// let app: express.Express; +const mockCatalog: jest.Mocked = { + getEntityByRef: jest.fn(), +} as any as jest.Mocked; -// beforeAll(async () => { -// const router = await createRouter({ -// logger: mockServices.logger.mock(), -// config: mockServices.rootConfig(), -// userInfo: mockServices.userInfo(), -// catalogApi: CatalogApi -// }); -// app = express().use(router); -// }); +describe('createRouter', () => { + let app: express.Express; -// beforeEach(() => { -// jest.resetAllMocks(); -// }); + beforeAll(async () => { + const router = await createRouter({ + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + userInfo: mockServices.userInfo(), + catalogApi: mockCatalog, + permissions: mockServices.permissions.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + }); + app = express().use(router); + }); -// describe('GET /health', () => { -// it('returns ok', async () => { -// const response = await request(app).get('/health'); + beforeEach(() => { + jest.resetAllMocks(); + }); -// expect(response.status).toEqual(200); -// expect(response.body).toEqual({ status: 'ok' }); -// }); -// }); -// }); + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/backstage-plugins/plugins/aws-apps-backend/src/service/router.ts b/backstage-plugins/plugins/aws-apps-backend/src/service/router.ts index a6ac8c34..b00ca569 100644 --- a/backstage-plugins/plugins/aws-apps-backend/src/service/router.ts +++ b/backstage-plugins/plugins/aws-apps-backend/src/service/router.ts @@ -9,21 +9,26 @@ import { readOpaAppAuditPermission, } from '@aws/plugin-aws-apps-common-for-backstage'; import { NotAllowedError } from '@backstage/errors'; -import {AuthService, HttpAuthService, LoggerService, PermissionsService, UserInfoService} from '@backstage/backend-plugin-api' +import { + AuthService, + HttpAuthService, + LoggerService, + PermissionsService, + UserInfoService, +} from '@backstage/backend-plugin-api'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import express from 'express'; import Router from 'express-promise-router'; import YAML from 'yaml'; -import { AwsAppsApi, getAWScreds } from '../api'; -import { AwsAuditResponse, createAuditRecord } from '../api/aws-audit'; +import { AwsAppsApi, AwsAuditResponse, createAuditRecord, getAWScreds } from '../api'; import { AwsAppsPlatformApi } from '../api/aws-platform'; import { Config } from '@backstage/config'; import { CatalogApi } from '@backstage/catalog-client'; - +/** @public */ export interface RouterOptions { - config: Config; + config: Config; logger: LoggerService; userInfo: UserInfoService; catalogApi: CatalogApi; @@ -32,60 +37,78 @@ export interface RouterOptions { httpAuth: HttpAuthService; } +/** @public */ export async function createRouter(options: RouterOptions): Promise { - const { config, logger, userInfo, catalogApi, permissions, auth, httpAuth} = options; - + const { config, logger, userInfo, catalogApi, permissions, auth, httpAuth } = options; + const permissionIntegrationRouter = createPermissionIntegrationRouter({ - permissions: [readOpaAppAuditPermission] + permissions: [readOpaAppAuditPermission], }); const router = Router(); router.use(permissionIntegrationRouter); - //Async function to get backend API client - async function getApiClient( - req: any, - ): Promise<{ apiClient: AwsAppsApi; apiClientConfig: ApiClientConfig }> { + // Async function to get backend API client + async function getApiClient(req: any): Promise<{ apiClient: AwsAppsApi; apiClientConfig: ApiClientConfig }> { const awsRegion = req.body.awsRegion?.toString(); const awsAccount = req.body.awsAccount?.toString(); const prefix = req.body.prefix?.toString(); const providerName = req.body.providerName?.toString(); const appName = req.body.appName?.toString(); - if (awsRegion === undefined || awsAccount === undefined || prefix === undefined || providerName === undefined || appName === undefined) { + if ( + awsRegion === undefined || + awsAccount === undefined || + prefix === undefined || + providerName === undefined || + appName === undefined + ) { throw Error; } const credentials = await httpAuth.credentials(req); const identity = await userInfo.getUserInfo(credentials); - const creds = await getAWScreds(awsAccount, awsRegion, prefix, providerName, undefined, identity); + const creds = await getAWScreds(config, logger, awsAccount, awsRegion, prefix, providerName, undefined, identity); const roleArn = creds.roleArn; - const apiClient = new AwsAppsApi(logger, creds.credentials, awsRegion, awsAccount); - + const apiClient = new AwsAppsApi(config, logger, awsRegion, awsAccount); + const requester = identity?.userEntityRef.split('/')[1] || ''; const owner = creds.owner ?? ''; - return { apiClient, apiClientConfig: { apiClient, roleArn, awsAccount, awsRegion, requester, owner, prefix, providerName, appName } }; + return { + apiClient, + apiClientConfig: { + apiClient, + roleArn, + awsAccount, + awsRegion, + requester, + owner, + prefix, + providerName, + appName, + }, + }; } function getAwsAppsPlatformApi(req: any): AwsAppsPlatformApi { const awsRegion = req.body.awsRegion?.toString(); const awsAccount = req.body.awsAccount?.toString(); - const platformRegion = process.env.AWS_REGION || config.getString('backend.platformRegion'); + const platformRegion = process.env.AWS_REGION ?? config.getString('backend.platformRegion'); const repoInfo: IRepositoryInfo = req.body.repoInfo; if (awsRegion === undefined || awsAccount === undefined) { throw new Error('getAwsAppsPlatformApi: awsRegion or awsAccount is undefined'); } if (repoInfo === undefined) { - return new AwsAppsPlatformApi(logger, platformRegion, awsRegion, awsAccount, GitProviders.UNSET); + return new AwsAppsPlatformApi(config, logger, platformRegion, awsRegion, awsAccount, GitProviders.UNSET); } - return new AwsAppsPlatformApi(logger, platformRegion, awsRegion, awsAccount, repoInfo.gitProvider); + return new AwsAppsPlatformApi(config, logger, platformRegion, awsRegion, awsAccount, repoInfo.gitProvider); } function getCloudFormationStackParams(req: any) { @@ -102,16 +125,18 @@ export async function createRouter(options: RouterOptions): Promise { - - const auditResponse = await createAuditRecord({ + async function createRouterAuditRecord({ + actionType, + actionName, + status, + apiClientConfig, + }: { + actionType: string; + actionName: string; + status: string; + apiClientConfig: ApiClientConfig; + }): Promise { + return createAuditRecord({ actionType, actionName, appName: apiClientConfig.appName, @@ -124,10 +149,8 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /ecs'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -147,11 +170,11 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /ecs/updateService'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -173,27 +196,21 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /secrets'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -203,11 +220,11 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: git/promote'); const apiPlatformClient = getAwsAppsPlatformApi(req); @@ -287,15 +304,15 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /platform/bind-resource'); const apiPlatformClient = getAwsAppsPlatformApi(req); @@ -315,15 +332,15 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /platform/unbind-resource'); const apiPlatformClient = getAwsAppsPlatformApi(req); @@ -343,15 +360,15 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /platform/update-provider'); const apiPlatformClient = getAwsAppsPlatformApi(req); @@ -360,33 +377,37 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /platform/fetch-eks-config'); - console.log(req.body) + console.log(req.body); const apiPlatformClient = getAwsAppsPlatformApi(req); const secretName = req.body.gitAdminSecret?.toString(); const envName = req.body.envName?.toString(); @@ -397,11 +418,11 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /resource-group'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -420,7 +441,7 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /ssm-parameter'); const { apiClient, apiClientConfig } = await getApiClient(req); const ssmParamName = req.body.ssmParamName?.toString(); const serviceResult = await apiClient.getSSMParameter(ssmParamName); - let status = "" + let status: string; if (serviceResult.$metadata.httpStatusCode === 200) { - status = 'SUCCESS' + status = 'SUCCESS'; } else { - status = 'FAILED' - console.error(`Failed fetching ${ssmParamName} on client ${apiClientConfig}`) + status = 'FAILED'; + console.error(`Failed fetching ${ssmParamName} on client ${apiClientConfig}`); } const auditResponse = await createRouterAuditRecord({ @@ -462,8 +483,7 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /logs/stream-events'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -504,14 +524,14 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /ecs/updateTaskDefinition'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -520,28 +540,35 @@ export async function createRouter(options: RouterOptions): Promise { - return { ...td, name: envVar[index].containerName, environment: envVar[index].env } + return { + ...td, + name: envVar[index].containerName, + environment: envVar[index].env, + }; }); - const newTd = {...oldTaskDefinition.taskDefinition, containerDefinitions: newCd }; + const newTd = { + ...oldTaskDefinition.taskDefinition, + containerDefinitions: newCd, + }; const output = await apiClient.registerTaskDefinition(newTd); const auditResponse = await createRouterAuditRecord({ actionType: 'Update TaskDefinition', actionName: taskDefinition, - status: output.$metadata.httpStatusCode == 200 ? 'SUCCESS' : 'FAILED', + status: output.$metadata.httpStatusCode === 200 ? 'SUCCESS' : 'FAILED', apiClientConfig, }); - if (auditResponse.status == 'FAILED') res.status(500).json({ message: 'auditing request FAILED.' }); + if (auditResponse.status === 'FAILED') res.status(500).json({ message: 'auditing request FAILED.' }); res.status(200).json(output.taskDefinition); }); - //Route for getting ECS taskdefinition details + // Route for getting ECS task definition details router.post('/ecs/describeTaskDefinition', async (req, res) => { logger.info('router entry: /ecs/describeTaskDefinition'); const { apiClient } = await getApiClient(req); const taskDefinition = req.body.taskDefinition?.toString(); - const taskDefinitionOutout = await apiClient.describeTaskDefinition(taskDefinition); - res.status(200).json(taskDefinitionOutout.taskDefinition); + const taskDefinitionOutput = await apiClient.describeTaskDefinition(taskDefinition); + res.status(200).json(taskDefinitionOutput.taskDefinition); }); // Route for getting Audit table entries @@ -549,8 +576,7 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /dynamo-db/query'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -623,15 +644,15 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /cloudformation/describeStack'); const { apiClient } = await getApiClient(req); @@ -644,7 +665,6 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /cloudformation/updateStack'); const { apiClient, apiClientConfig } = await getApiClient(req); - const { - componentName, stackName, s3BucketName, cfFileName, providerName, - envName - } = getCloudFormationStackParams(req); + const { componentName, stackName, s3BucketName, cfFileName, providerName, envName } = + getCloudFormationStackParams(req); const gitSecretName = req.body.gitAdminSecret?.toString(); const repoInfo: IRepositoryInfo = req.body.repoInfo; const filePath = encodeURIComponent(`.awsdeployment/stackparams/${envName}-${providerName}.json`); @@ -674,36 +692,42 @@ export async function createRouter(options: RouterOptions): Promise `${prevVal} "${currVal.ParameterKey}=${currVal.ParameterValue}"`, '').trim(); + const params = parameters! + .reduce((prevVal, currVal) => `${prevVal} "${currVal.ParameterKey}=${currVal.ParameterValue}"`, '') + .trim(); logger.info(`CloudFormation parameter overrides: ${params}`); } - const stackOutput = await apiClient.updateStack(componentName, stackName, s3BucketName, cfFileName, providerName, parameters); + const stackOutput = await apiClient.updateStack( + componentName, + stackName, + s3BucketName, + cfFileName, + providerName, + parameters, + ); const auditResponse = await createRouterAuditRecord({ actionType: 'Update Stack', actionName: componentName, - status: stackOutput.$metadata.httpStatusCode == 200 ? 'SUCCESS' : 'FAILED', + status: stackOutput.$metadata.httpStatusCode === 200 ? 'SUCCESS' : 'FAILED', apiClientConfig, }); - if (auditResponse.status == 'FAILED') res.status(500).json({ message: 'auditing request FAILED.' }); + if (auditResponse.status === 'FAILED') res.status(500).json({ message: 'auditing request FAILED.' }); res.status(200).json(stackOutput); - }); // Route for creating a CloudFormation stack router.post('/cloudformation/createStack', async (req, res) => { logger.info('router entry: /cloudformation/createStack'); const { apiClient, apiClientConfig } = await getApiClient(req); - const { - componentName, stackName, s3BucketName, cfFileName, providerName, - envName - } = getCloudFormationStackParams(req); + const { componentName, stackName, s3BucketName, cfFileName, providerName, envName } = + getCloudFormationStackParams(req); const repoInfo: IRepositoryInfo = req.body.repoInfo; const gitSecretName = req.body.gitAdminSecret?.toString(); const filePath = encodeURIComponent(`.awsdeployment/stackparams/${envName}-${providerName}.json`); @@ -712,29 +736,37 @@ export async function createRouter(options: RouterOptions): Promise `${prevVal} "${currVal.ParameterKey}=${currVal.ParameterValue}"`, '').trim(); + const params = parameters! + .reduce((prevVal, currVal) => `${prevVal} "${currVal.ParameterKey}=${currVal.ParameterValue}"`, '') + .trim(); logger.info(`CloudFormation parameter overrides: ${params}`); } - const stackOutput = await apiClient.createStack(componentName, stackName, s3BucketName, cfFileName, providerName, parameters); + const stackOutput = await apiClient.createStack( + componentName, + stackName, + s3BucketName, + cfFileName, + providerName, + parameters, + ); const auditResponse = await createRouterAuditRecord({ actionType: 'Create Stack', actionName: componentName, - status: stackOutput.$metadata.httpStatusCode == 200 ? 'SUCCESS' : 'FAILED', + status: stackOutput.$metadata.httpStatusCode === 200 ? 'SUCCESS' : 'FAILED', apiClientConfig, }); - if (auditResponse.status == 'FAILED') { + if (auditResponse.status === 'FAILED') { res.status(500).json({ message: 'auditing request FAILED.' }); } res.status(200).json(stackOutput); - }); // Route for deleting a CloudFormation stack @@ -750,14 +782,13 @@ export async function createRouter(options: RouterOptions): Promise { logger.info('router entry: /lambda/invoke'); const { apiClient, apiClientConfig } = await getApiClient(req); @@ -786,20 +816,19 @@ export async function createRouter(options: RouterOptions): Promise { -// const logger = options.logger.child({ service: 'aws-apps-backend' }); -// logger.debug('Starting application server...'); -// const config = await loadBackendConfig({ logger, argv: process.argv }); -// const discovery = HostDiscovery.fromConfig(config); -// const tokenManager = ServerTokenManager.fromConfig(config, { -// logger, -// }); -// const permissions = ServerPermissionClient.fromConfig(config, { -// discovery, -// tokenManager, -// }); -// const router = await createRouter({ -// logger, -// userInfo, -// config, -// permissions, -// }); - -// let service = createServiceBuilder(module).setPort(options.port).addRouter('/aws-apps-backend', router); -// if (options.enableCors) { -// service = service.enableCors({ origin: 'http://localhost:3000' }); -// } - -// return await service.start().catch(err => { -// logger.error(err); -// process.exit(1); -// }); -// } - -// module.hot?.accept(); diff --git a/backstage-plugins/plugins/aws-apps-common/.eslintrc.js b/backstage-plugins/plugins/aws-apps-common/.eslintrc.js new file mode 100644 index 00000000..e2a53a6a --- /dev/null +++ b/backstage-plugins/plugins/aws-apps-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/backstage-plugins/plugins/aws-apps-common/README.md b/backstage-plugins/plugins/aws-apps-common/README.md index 19b0ef4f..108b1570 100644 --- a/backstage-plugins/plugins/aws-apps-common/README.md +++ b/backstage-plugins/plugins/aws-apps-common/README.md @@ -1,4 +1,4 @@ - @@ -9,5 +9,5 @@ Shared isomorphic code for the aws-apps plugins. ## Links -- [Frontend part of aws-apps](https://www.npmjs.com/package/@aws/plugin-aws-apps-for-backstage) -- [Backend part of aws-apps](https://www.npmjs.com/package/@aws/plugin-aws-apps-backend-for-backstage) \ No newline at end of file +- [Frontend part of aws-apps](https://www.npmjs.com/package/@alithya-oss/plugin-aws-apps) +- [Backend part of aws-apps](https://www.npmjs.com/package/@alithya-oss/plugin-aws-apps-backend) diff --git a/backstage-plugins/plugins/aws-apps-common/api-report.md b/backstage-plugins/plugins/aws-apps-common/api-report.md new file mode 100644 index 00000000..7aa32de4 --- /dev/null +++ b/backstage-plugins/plugins/aws-apps-common/api-report.md @@ -0,0 +1,548 @@ +## API Report File for "@aws/plugin-aws-apps-common-for-backstage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; +import { Entity } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/types'; +import { KindValidator } from '@backstage/catalog-model'; +import { StackStatus } from '@aws-sdk/client-cloudformation'; + +// @public (undocumented) +export type AppPromoParams = { + envName: string; + envRequiresManualApproval: boolean; + appName: string; + providers: AWSProviderParams[]; +}; + +// @public (undocumented) +export type AppState = { + appID?: string; + appState?: AppStateType; + deploymentIdentifier?: string; + runningCount?: number; + desiredCount?: number; + pendingCount?: number; + lastStateTimestamp?: Date; + stateObject?: any; + additionalInfo?: KeyValue[]; +}; + +// @public (undocumented) +export enum AppStateType { + // (undocumented) + PROVISIONING = 'Provisioning', + // (undocumented) + RUNNING = 'Running', + // (undocumented) + STOPPED = 'Stopped', + // (undocumented) + UPDATING = 'Updating', +} + +// @public (undocumented) +export interface AssociatedResources { + // (undocumented) + resourceArn: string; + // (undocumented) + resourceName: string; + // (undocumented) + resourceType: string; +} + +// @public (undocumented) +export interface AuditRecord { + // (undocumented) + actionName: string; + // (undocumented) + actionType: string; + // (undocumented) + appName: string; + // (undocumented) + assumedRole: string; + // (undocumented) + createdAt: string; + // (undocumented) + createdDate: string; + // (undocumented) + id: string; + // (undocumented) + initiatedBy: string; + // (undocumented) + message: string; + // (undocumented) + origin: string; + // (undocumented) + owner: string; + // (undocumented) + prefix: string; + // (undocumented) + providerName: string; + // (undocumented) + request: string; + // (undocumented) + status: string; + // (undocumented) + targetAccount: string; + // (undocumented) + targetRegion: string; +} + +// @public +export type AWSComponent = { + componentName: string; + componentType: AWSComponentType; + componentSubType: string; + iacType: string; + componentState: ComponentStateType; + repoSecretArn: string; + platformRegion: string; + environments: AwsDeploymentEnvironments; + currentEnvironment: GenericAWSEnvironment; + setCurrentProvider: (envName: string, providerName: string) => void; + getRepoInfo: () => IRepositoryInfo; +}; + +// @public (undocumented) +export enum AWSComponentType { + // (undocumented) + AWSApp = 'aws-app', + // (undocumented) + AWSEnvironment = 'aws-environment', + // (undocumented) + AWSOrganization = 'aws-organization', + // (undocumented) + AWSProvider = 'aws-provider', + // (undocumented) + AWSResource = 'aws-resource', + // (undocumented) + Default = 'aws-default', +} + +// @public +export type AWSDeploymentEnvironment = { + providerData: { + name: string; + prefix: string; + description: string; + accountNumber: string; + region: string; + vpcSsmKey: string; + providerType: string; + auditTable: string; + operationRoleSsmKey: string; + provisioningRoleSsmKey: string; + cloudFormationStackName: string; + terraformWorkspace: string; + terraformStateBucket: string; + terraformStateTable: string; + }; + environment: { + name: string; + description: string; + accountType: string; + regionType: string; + category: string; + classification: string; + envType: string; + level: number; + }; + entities: { + envEntity?: AWSEnvironmentEntityV1; + envProviderEntity?: AWSEnvironmentProviderEntityV1; + }; + app: AWSDeploymentEnvironmentComponent; + resource: {}; +}; + +// @public (undocumented) +export type AWSDeploymentEnvironmentComponent = { + cloudFormationStackName: string; + links: { + title: string; + url: string; + icon?: string; + }[]; +}; + +// @public +export type AwsDeploymentEnvironments = { + [key: string]: GenericAWSEnvironment; +}; + +// @public (undocumented) +export type AWSECSAppDeploymentEnvironment = AWSDeploymentEnvironment & { + clusterName: string; + app: AWSDeploymentEnvironmentComponent & { + ecrArn: string; + serviceArn: string; + taskDefArn: string; + taskExecutionRoleArn: string; + resourceGroupArn: string; + logGroupName: string; + }; +}; + +// @public (undocumented) +export type AWSEKSAppDeploymentEnvironment = AWSDeploymentEnvironment & { + clusterName: string; + app: AWSDeploymentEnvironmentComponent & { + appAdminRoleArn: string; + ecrArn: string; + namespace: string; + resourceGroupArn: string; + logGroupName: string; + }; +}; + +// @public +export interface AWSEnvironmentEntityV1 extends Entity { + apiVersion: 'aws.backstage.io/v1alpha'; + kind: 'AWSEnvironment'; + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + system?: string; + lifecycle: string; + output?: { + [name: string]: string; + }; + owner?: string; + dependsOn?: string[]; + }; +} + +// @public +export const awsEnvironmentEntityV1Validator: KindValidator; + +// @public +export interface AWSEnvironmentProviderEntityV1 extends Entity { + apiVersion: 'aws.backstage.io/v1alpha'; + kind: 'AWSEnvironmentProvider'; + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + system?: string; + lifecycle: string; + output?: { + [name: string]: string; + }; + owner?: string; + }; +} + +// @public +export const awsEnvironmentProviderEntityV1Validator: KindValidator; + +// @public (undocumented) +export type AWSEnvironmentProviderRecord = { + id: string; + name: string; + prefix: string; + providerType: string; + description: string; + accountNumber: string; + region: string; +}; + +// @public (undocumented) +export type AWSProviderParams = { + awsAccount: string; + awsRegion: string; + assumedRoleArn: string; + environmentName: string; + envRequiresManualApproval: boolean; + prefix: string; + providerName: string; + parameters: { + [key: string]: string; + }; +}; + +// @public (undocumented) +export interface AWSResource { + // (undocumented) + resourceArn: string; + // (undocumented) + resourceName: string; + // (undocumented) + resourceTypeId: string; + // (undocumented) + resourceTypeName: string; +} + +// @public (undocumented) +export type AWSResourceDeploymentEnvironment = AWSDeploymentEnvironment & { + resource: { + arn: string; + resourceName: string; + resourceType: string; + resourceGroupArn: string; + cloudFormationStackName: string; + }; +}; + +// @public (undocumented) +export type AWSServerlessAppDeploymentEnvironment = AWSDeploymentEnvironment & { + app: { + appStack: CloudFormationStack; + s3BucketName?: string; + resourceGroupArn: string; + logGroupNames: string[]; + }; +}; + +// @public (undocumented) +export interface AWSServiceResources { + // (undocumented) + [k: string]: Array; +} + +// @public +export type BackendParams = { + awsAccount: string; + awsRegion: string; + prefix: string; + providerName: string; + appName: string; +}; + +// @public (undocumented) +export interface BindResourceParams { + // (undocumented) + appName: string; + // (undocumented) + envName: string; + // (undocumented) + policies: ResourcePolicy[]; + // (undocumented) + providerName: string; + // (undocumented) + resourceEntityRef: string; + // (undocumented) + resourceName: string; +} + +// @public +export type CloudFormationStack = { + stackDeployStatus: DeployStackStatus; + stackName: string; + creationTime?: string; + lastUpdatedTime?: string; +}; + +// @public (undocumented) +export enum ComponentStateType { + // (undocumented) + CLOUDFORMATION = 'cloudformation', + // (undocumented) + TERRAFORM_AWS = 'terraform-aws', + // (undocumented) + TERRAFORM_CLOUD = 'terraform-cloud', +} + +// @public (undocumented) +export type DeployStackStatus = StackStatus | ExtraStackDeployStatus; + +// @public (undocumented) +export enum ExtraStackDeployStatus { + // (undocumented) + STAGED = 'STAGED', + // (undocumented) + UNSTAGED = 'UNSTAGED', +} + +// @public (undocumented) +export type GenericAWSEnvironment = + | AWSDeploymentEnvironment + | AWSECSAppDeploymentEnvironment + | AWSEKSAppDeploymentEnvironment + | AWSServerlessAppDeploymentEnvironment + | AWSResourceDeploymentEnvironment; + +// @public (undocumented) +export const getGitCredentailsSecret: (repoInfo: IRepositoryInfo) => string; + +// @public (undocumented) +export const getRepoInfo: (entity: Entity) => IRepositoryInfo; + +// @public (undocumented) +export const getRepoUrl: (repoInfo: IRepositoryInfo) => string; + +// @public (undocumented) +export enum GitProviders { + // (undocumented) + GITHUB = 'github', + // (undocumented) + GITLAB = 'gitlab', + // (undocumented) + UNSET = 'unset', +} + +// @public (undocumented) +export enum GitVisibility { + // (undocumented) + PRIVATE = 'private', + // (undocumented) + PUBIC = 'public', +} + +// @public (undocumented) +export interface ICommitAction { + // (undocumented) + action: string; + // (undocumented) + content: string; + // (undocumented) + file_path: string; +} + +// @public (undocumented) +export interface ICommitChange { + // (undocumented) + actions: ICommitAction[]; + // (undocumented) + branch: string; + // (undocumented) + commitMessage: string; +} + +// @public (undocumented) +export interface IGitAPIResult { + // (undocumented) + httpResponse: number; + // (undocumented) + isSuccess: boolean; + // (undocumented) + message: string; + // (undocumented) + value: any; +} + +// @public (undocumented) +export interface IRepositoryInfo { + // (undocumented) + description?: string; + // (undocumented) + gitHost: string; + // (undocumented) + gitJobID?: string; + // (undocumented) + gitOrganization?: string; + // (undocumented) + gitProjectGroup?: string; + // (undocumented) + gitProvider: GitProviders; + // (undocumented) + gitRepoName: string; + // (undocumented) + isPrivate: boolean; + // (undocumented) + projectID?: string; + // (undocumented) + rawIdentifier?: string; + // (undocumented) + visibility?: GitVisibility; +} + +// @public (undocumented) +export function isAWSECSAppDeploymentEnvironment( + variable: any, +): variable is AWSECSAppDeploymentEnvironment; + +// @public (undocumented) +export function isAWSEKSAppDeploymentEnvironment( + variable: any, +): variable is AWSEKSAppDeploymentEnvironment; + +// @public (undocumented) +export function isAWSServerlessAppDeploymentEnvironment( + variable: any, +): variable is AWSServerlessAppDeploymentEnvironment; + +// @public (undocumented) +export interface ISCMBackendAPI { + // (undocumented) + commitContent: ( + change: ICommitChange, + repo: IRepositoryInfo, + accessToken: string, + ) => Promise; + // (undocumented) + createRepository: ( + repo: IRepositoryInfo, + accessToken: string, + ) => Promise; + // (undocumented) + deleteRepository: ( + repo: IRepositoryInfo, + accessToken: string, + ) => Promise; + // (undocumented) + getFileContent: ( + filePath: string, + repo: IRepositoryInfo, + accessToken: string, + ) => Promise; +} + +// @public (undocumented) +export interface KeyValue { + // (undocumented) + id: string; + // (undocumented) + key: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface KeyValueDouble { + // (undocumented) + id: string; + // (undocumented) + key: string; + // (undocumented) + key2: string; + // (undocumented) + value: string; + // (undocumented) + value2: string; +} + +// @public (undocumented) +export const opaPermissions: BasicPermission[]; + +// @public (undocumented) +export const readOpaAppAuditPermission: BasicPermission; + +// @public (undocumented) +export interface ResourceBinding { + // (undocumented) + associatedResources?: AssociatedResources[]; + // (undocumented) + entityRef?: string; + // (undocumented) + id: string; + // (undocumented) + provider: string; + // (undocumented) + resourceArn: string; + // (undocumented) + resourceName: string; + // (undocumented) + resourceType: string; +} + +// @public (undocumented) +export interface ResourcePolicy { + // (undocumented) + policyContent: string; + // (undocumented) + policyFileName: string; + // (undocumented) + policyResource: string; +} +``` diff --git a/backstage-plugins/plugins/aws-apps-common/config.d.ts b/backstage-plugins/plugins/aws-apps-common/config.d.ts index dd09a77d..9f44a063 100644 --- a/backstage-plugins/plugins/aws-apps-common/config.d.ts +++ b/backstage-plugins/plugins/aws-apps-common/config.d.ts @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 export interface Config { - backend: { - /** - * Frontend root URL - * @visibility frontend - */ - platformRegion: string; - }; - } \ No newline at end of file + backend: { + /** + * Frontend root URL + * @visibility frontend + */ + platformRegion: string; + }; +} diff --git a/backstage-plugins/plugins/aws-apps-common/package.json b/backstage-plugins/plugins/aws-apps-common/package.json index bbfe2fb9..6a5cef10 100644 --- a/backstage-plugins/plugins/aws-apps-common/package.json +++ b/backstage-plugins/plugins/aws-apps-common/package.json @@ -26,8 +26,11 @@ "backstage": { "role": "common-library", "pluginId": "aws-apps-common", - "pluginPackages": ["@aws/plugin-aws-apps-common-for-backstage"] + "pluginPackages": [ + "@aws/plugin-aws-apps-common-for-backstage" + ] }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -36,8 +39,7 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, - "dependencies": { - }, + "dependencies": {}, "devDependencies": { "@backstage/cli": "^0.26.11" }, diff --git a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.schema.json b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.schema.json index a8f363fe..e60552a3 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.schema.json +++ b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.schema.json @@ -41,7 +41,7 @@ "properties": { "type": { "type": "string", - "description": "The type of the Environment", + "description": "The type of the Environment", "minLength": 1 }, "lifecycle": { @@ -74,4 +74,4 @@ } } ] -} \ No newline at end of file +} diff --git a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.ts b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.ts index d5688c1b..4a53742f 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentEntityV1.ts @@ -1,68 +1,64 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - Entity, - entityKindSchemaValidator, - KindValidator } from '@backstage/catalog-model'; - import { JsonObject } from '@backstage/types'; - import schema from './AWSEnvironmentEntityV1.schema.json'; +import { Entity, entityKindSchemaValidator, KindValidator } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/types'; +import schema from './AWSEnvironmentEntityV1.schema.json'; +/** + * Backstage catalog Template kind Entity. Templates are used by the Scaffolder + * plugin to create new entities, such as Components. + * + * @public + */ +export interface AWSEnvironmentEntityV1 extends Entity { /** - * Backstage catalog Template kind Entity. Templates are used by the Scaffolder - * plugin to create new entities, such as Components. - * - * @public + * The apiVersion string of the TaskSpec. */ - export interface AWSEnvironmentEntityV1 extends Entity { + apiVersion: 'aws.backstage.io/v1alpha'; + /** + * The kind of the entity + */ + kind: 'AWSEnvironment'; + /** + * The specification of the Template Entity + */ + spec: { /** - * The apiVersion string of the TaskSpec. + * The type that the Template will create. For example service, website or library. */ - apiVersion: 'aws.backstage.io/v1alpha'; + type: string; /** - * The kind of the entity + * This is a JSONSchema or an array of JSONSchema's which is used to render a form in the frontend + * to collect user input and validate it against that schema. This can then be used in the `steps` part below to template + * variables passed from the user into each action in the template. */ - kind: 'AWSEnvironment'; + parameters?: JsonObject | JsonObject[]; + + system?: string; + lifecycle: string; /** - * The specification of the Template Entity + * The output is an object where template authors can pull out information from template actions and return them in a known standard way. */ - spec: { - /** - * The type that the Template will create. For example service, website or library. - */ - type: string; - /** - * This is a JSONSchema or an array of JSONSchema's which is used to render a form in the frontend - * to collect user input and validate it against that schema. This can then be used in the `steps` part below to template - * variables passed from the user into each action in the template. - */ - parameters?: JsonObject | JsonObject[]; - - system?: string; - lifecycle: string; - /** - * The output is an object where template authors can pull out information from template actions and return them in a known standard way. - */ - output?: { [name: string]: string }; - /** - * The owner entityRef of the TemplateEntity - */ - owner?: string; - dependsOn?: string[]; - }; - } - - const validator = entityKindSchemaValidator(schema); - - /** - * Entity data validator for {@link AWSEnvironmentEntityV1}. - * - * @public - */ - export const awsEnvironmentEntityV1Validator: KindValidator = { - // TODO(freben): Emulate the old KindValidator until we fix that type - async check(data: Entity) { - return validator(data) === data; - }, + output?: { [name: string]: string }; + /** + * The owner entityRef of the TemplateEntity + */ + owner?: string; + dependsOn?: string[]; }; +} + +const validator = entityKindSchemaValidator(schema); +/** + * Entity data validator for {@link AWSEnvironmentEntityV1}. + * + * @public + */ +export const awsEnvironmentEntityV1Validator: KindValidator = { + // TODO(freben): Emulate the old KindValidator until we fix that type + async check(data: Entity) { + return validator(data) === data; + }, +}; diff --git a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.schema.json b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.schema.json index 87419df2..03a50bf6 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.schema.json +++ b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.schema.json @@ -1,68 +1,68 @@ { - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "AWSEnvironmentProviderEntityV1Alpha", - "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", - "examples": [ - { - "apiVersion": "aws.backstage.io/v1alpha", - "kind": "AWSEnvironmentProvider", - "metadata": { - "name": "aws-environment-provider", - "title": "AWS environment provider", - "description": "An AWS environment provider entity", - "tags": ["aws"] + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AWSEnvironmentProviderEntityV1Alpha", + "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", + "examples": [ + { + "apiVersion": "aws.backstage.io/v1alpha", + "kind": "AWSEnvironmentProvider", + "metadata": { + "name": "aws-environment-provider", + "title": "AWS environment provider", + "description": "An AWS environment provider entity", + "tags": ["aws"] + }, + "spec": { + "type": "aws-environment-provider", + "lifecycle": "experimental", + "owner": "group:dev-ops", + "system": "demo" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["aws.backstage.io/v1alpha"] + }, + "kind": { + "enum": ["AWSEnvironmentProvider"] }, "spec": { - "type": "aws-environment-provider", - "lifecycle": "experimental", - "owner": "group:dev-ops", - "system": "demo" - } - } - ], - "allOf": [ - { - "$ref": "Entity" - }, - { - "type": "object", - "required": ["spec"], - "properties": { - "apiVersion": { - "enum": ["aws.backstage.io/v1alpha"] - }, - "kind": { - "enum": ["AWSEnvironmentProvider"] - }, - "spec": { - "type": "object", - "required": ["type", "lifecycle", "owner"], - "properties": { - "type": { - "type": "string", - "description": "The type of the Environment", - "minLength": 1 - }, - "lifecycle": { - "type": "string", - "description": "The lifecycle state of the Environment.", - "examples": ["experimental", "production", "deprecated"], - "minLength": 1 - }, - "owner": { - "type": "string", - "description": "An entity reference to the owner of the Environment.", - "examples": ["artist-relations-team", "user:john.johnson"], - "minLength": 1 - }, - "system": { - "type": "string", - "description": "An entity reference to the system that the Environment belongs to.", - "minLength": 1 - } + "type": "object", + "required": ["type", "lifecycle", "owner"], + "properties": { + "type": { + "type": "string", + "description": "The type of the Environment", + "minLength": 1 + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the Environment.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the Environment.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the Environment belongs to.", + "minLength": 1 } } } } - ] - } \ No newline at end of file + } + ] +} diff --git a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.ts b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.ts index d3555319..994db373 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/entities/AWSEnvironmentProviderEntityV1.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - Entity, - entityKindSchemaValidator, - KindValidator, - } from '@backstage/catalog-model'; +import { Entity, entityKindSchemaValidator, KindValidator } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import schema from './AWSEnvironmentProviderEntityV1.schema.json'; diff --git a/backstage-plugins/plugins/aws-apps-common/src/helpers/constants.ts b/backstage-plugins/plugins/aws-apps-common/src/helpers/constants.ts new file mode 100644 index 00000000..fcd4c848 --- /dev/null +++ b/backstage-plugins/plugins/aws-apps-common/src/helpers/constants.ts @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { StackStatus } from '@aws-sdk/client-cloudformation'; + +/** @public */ +export enum ExtraStackDeployStatus { + STAGED = 'STAGED', + UNSTAGED = 'UNSTAGED', +} + +/** @public */ +export type DeployStackStatus = StackStatus | ExtraStackDeployStatus; diff --git a/backstage-plugins/plugins/aws-apps-common/src/index.ts b/backstage-plugins/plugins/aws-apps-common/src/index.ts index 3f64a29b..b3b79bc9 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/index.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/index.ts @@ -10,6 +10,10 @@ export * from './types'; export * from './permissions'; export * from './utils/git-util'; +export * from './helpers/constants'; -export { type AWSEnvironmentProviderEntityV1, awsEnvironmentProviderEntityV1Validator } from './entities/AWSEnvironmentProviderEntityV1'; -export { type AWSEnvironmentEntityV1,awsEnvironmentEntityV1Validator } from './entities/AWSEnvironmentEntityV1'; \ No newline at end of file +export { + type AWSEnvironmentProviderEntityV1, + awsEnvironmentProviderEntityV1Validator, +} from './entities/AWSEnvironmentProviderEntityV1'; +export { type AWSEnvironmentEntityV1, awsEnvironmentEntityV1Validator } from './entities/AWSEnvironmentEntityV1'; diff --git a/backstage-plugins/plugins/aws-apps-common/src/permissions.ts b/backstage-plugins/plugins/aws-apps-common/src/permissions.ts index ea0bbdf8..3ad77ef6 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/permissions.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/permissions.ts @@ -3,6 +3,7 @@ import { createPermission } from '@backstage/plugin-permission-common'; +/** @public */ export const readOpaAppAuditPermission = createPermission({ name: 'opa.app.audit.read', attributes: { @@ -10,4 +11,5 @@ export const readOpaAppAuditPermission = createPermission({ }, }); +/** @public */ export const opaPermissions = [readOpaAppAuditPermission]; diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/AWSResource.ts b/backstage-plugins/plugins/aws-apps-common/src/types/AWSResource.ts index 04525652..5d9f5594 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/AWSResource.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/AWSResource.ts @@ -11,7 +11,7 @@ export interface AWSResource { resourceArn: string; } - +/** @public */ export interface AuditRecord { id: string; origin: string; @@ -25,41 +25,44 @@ export interface AuditRecord { assumedRole: string; targetAccount: string; targetRegion: string; - prefix:string; - providerName:string; + prefix: string; + providerName: string; request: string; status: string; message: string; } +/** @public */ export interface ResourceBinding { id: string; resourceType: string; resourceName: string; provider: string; resourceArn: string; - associatedResources?: AssociatedResources[] - entityRef?:string; + associatedResources?: AssociatedResources[]; + entityRef?: string; } +/** @public */ export interface AssociatedResources { resourceName: string; resourceType: string; resourceArn: string; } - +/** @public */ export interface BindResourceParams { envName: string; providerName: string; - resourceName:string; - resourceEntityRef:string; + resourceName: string; + resourceEntityRef: string; policies: ResourcePolicy[]; appName: string; } +/** @public */ export interface ResourcePolicy { - policyFileName:string; + policyFileName: string; policyContent: string; - policyResource:string; -} \ No newline at end of file + policyResource: string; +} diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/AWSServiceResources.ts b/backstage-plugins/plugins/aws-apps-common/src/types/AWSServiceResources.ts index ae99dc60..c17f3b5b 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/AWSServiceResources.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/AWSServiceResources.ts @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AWSResource } from "./AWSResource"; +import { AWSResource } from './AWSResource'; /** * @public */ export interface AWSServiceResources { [k: string]: Array; -} \ No newline at end of file +} diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/AWSUIInterfaces.ts b/backstage-plugins/plugins/aws-apps-common/src/types/AWSUIInterfaces.ts index 1fe9fc14..fa1ea3c1 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/AWSUIInterfaces.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/AWSUIInterfaces.ts @@ -1,15 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AWSEnvironmentEntityV1, } from "../entities/AWSEnvironmentEntityV1"; -import { AWSEnvironmentProviderEntityV1 } from "../entities/AWSEnvironmentProviderEntityV1" -import { DeployStackStatus } from "@aws/plugin-aws-apps-for-backstage/src/helpers/constants"; -import { IRepositoryInfo } from "./SCMBackendAPI"; +import { AWSEnvironmentEntityV1 } from '../entities/AWSEnvironmentEntityV1'; +import { AWSEnvironmentProviderEntityV1 } from '../entities/AWSEnvironmentProviderEntityV1'; +import { DeployStackStatus } from '../helpers/constants'; +import { IRepositoryInfo } from './SCMBackendAPI'; -/** +/** * A type for interacting with OPA Backend environments * @public -*/ + */ export type BackendParams = { awsAccount: string; awsRegion: string; @@ -18,29 +18,34 @@ export type BackendParams = { appName: string; }; - // Create providers definition for the different providers types: eks, ecs, and serverless -export type GenericAWSEnvironment = AWSDeploymentEnvironment | AWSECSAppDeploymentEnvironment | AWSEKSAppDeploymentEnvironment - | AWSServerlessAppDeploymentEnvironment | AWSResourceDeploymentEnvironment // the key is the environment name (in all lower case) +/** @public */ +export type GenericAWSEnvironment = + | AWSDeploymentEnvironment + | AWSECSAppDeploymentEnvironment + | AWSEKSAppDeploymentEnvironment + | AWSServerlessAppDeploymentEnvironment + | AWSResourceDeploymentEnvironment; // the key is the environment name (in all lower case) // ---------------------------------------------------------------- AWS Environment types below ------------------------------- -/** +/** * A collection(map) of associated environments of an AWS App or Resource * @public -*/ + */ export type AwsDeploymentEnvironments = { - [key: string]: GenericAWSEnvironment -} + [key: string]: GenericAWSEnvironment; +}; +/** @public */ export type AWSDeploymentEnvironmentComponent = { cloudFormationStackName: string; - links: { title: string, url: string, icon?: string }[]; -} + links: { title: string; url: string; icon?: string }[]; +}; -/** +/** * A single provider map for an AWS Component * @public -*/ + */ export type AWSDeploymentEnvironment = { providerData: { name: string; @@ -49,7 +54,7 @@ export type AWSDeploymentEnvironment = { accountNumber: string; region: string; vpcSsmKey: string; - providerType: string; // ecs/eks/serverless/ TBD + providerType: string; // ecs/eks/serverless/ TBD auditTable: string; operationRoleSsmKey: string; provisioningRoleSsmKey: string; @@ -57,7 +62,6 @@ export type AWSDeploymentEnvironment = { terraformWorkspace: string; terraformStateBucket: string; terraformStateTable: string; - }; environment: { name: string; @@ -74,24 +78,26 @@ export type AWSDeploymentEnvironment = { envProviderEntity?: AWSEnvironmentProviderEntityV1; }; app: AWSDeploymentEnvironmentComponent; - resource: {} -} + resource: {}; +}; +/** @public */ export function isAWSECSAppDeploymentEnvironment(variable: any): variable is AWSECSAppDeploymentEnvironment { return ( !!variable && - typeof variable === "object" && - "clusterName" in variable && - "app" in variable && - "ecrArn" in variable.app && - "serviceArn" in variable.app && - "taskDefArn" in variable.app && - "taskExecutionRoleArn" in variable.app && - "resourceGroupArn" in variable.app && - "logGroupName" in variable.app + typeof variable === 'object' && + 'clusterName' in variable && + 'app' in variable && + 'ecrArn' in variable.app && + 'serviceArn' in variable.app && + 'taskDefArn' in variable.app && + 'taskExecutionRoleArn' in variable.app && + 'resourceGroupArn' in variable.app && + 'logGroupName' in variable.app ); } +/** @public */ export type AWSECSAppDeploymentEnvironment = AWSDeploymentEnvironment & { clusterName: string; app: AWSDeploymentEnvironmentComponent & { @@ -101,22 +107,24 @@ export type AWSECSAppDeploymentEnvironment = AWSDeploymentEnvironment & { taskExecutionRoleArn: string; resourceGroupArn: string; logGroupName: string; - } -} + }; +}; +/** @public */ export function isAWSEKSAppDeploymentEnvironment(variable: any): variable is AWSEKSAppDeploymentEnvironment { return ( !!variable && - typeof variable === "object" && - "clusterName" in variable && - "app" in variable && - "ecrArn" in variable.app && - "namespace" in variable.app && - "resourceGroupArn" in variable.app && - "logGroupName" in variable.app + typeof variable === 'object' && + 'clusterName' in variable && + 'app' in variable && + 'ecrArn' in variable.app && + 'namespace' in variable.app && + 'resourceGroupArn' in variable.app && + 'logGroupName' in variable.app ); } +/** @public */ export type AWSEKSAppDeploymentEnvironment = AWSDeploymentEnvironment & { clusterName: string; app: AWSDeploymentEnvironmentComponent & { @@ -125,41 +133,46 @@ export type AWSEKSAppDeploymentEnvironment = AWSDeploymentEnvironment & { namespace: string; resourceGroupArn: string; logGroupName: string; - } -} + }; +}; -/** +/** * CloudFormation stack data * @public -*/ + */ export type CloudFormationStack = { stackDeployStatus: DeployStackStatus; stackName: string; creationTime?: string; lastUpdatedTime?: string; -} +}; -export function isAWSServerlessAppDeploymentEnvironment(variable: any): variable is AWSServerlessAppDeploymentEnvironment { +/** @public */ +export function isAWSServerlessAppDeploymentEnvironment( + variable: any, +): variable is AWSServerlessAppDeploymentEnvironment { return ( !!variable && - typeof variable === "object" && - "app" in variable && - "appStack" in variable.app && - "resourceGroupArn" in variable.app && - "logGroupNames" in variable.app + typeof variable === 'object' && + 'app' in variable && + 'appStack' in variable.app && + 'resourceGroupArn' in variable.app && + 'logGroupNames' in variable.app ); } +/** @public */ export type AWSServerlessAppDeploymentEnvironment = AWSDeploymentEnvironment & { app: { appStack: CloudFormationStack; // non-IaC stack that defines serverless resources like lambdas s3BucketName?: string; // name of S3 bucket that holds CICD build artifacts for use in deployment resourceGroupArn: string; logGroupNames: string[]; - } -} + }; +}; // capture an AWS resource withing a particular AWS Provider +/** @public */ export type AWSResourceDeploymentEnvironment = AWSDeploymentEnvironment & { resource: { arn: string; @@ -167,31 +180,31 @@ export type AWSResourceDeploymentEnvironment = AWSDeploymentEnvironment & { resourceType: string; resourceGroupArn: string; cloudFormationStackName: string; - } -} - + }; +}; +/** @public */ export enum ComponentStateType { - CLOUDFORMATION = "cloudformation", - TERRAFORM_CLOUD = "terraform-cloud", - TERRAFORM_AWS = "terraform-aws" + CLOUDFORMATION = 'cloudformation', + TERRAFORM_CLOUD = 'terraform-cloud', + TERRAFORM_AWS = 'terraform-aws', } - +/** @public */ export enum AWSComponentType { - AWSApp = "aws-app", - AWSResource = "aws-resource", - AWSOrganization = "aws-organization", - AWSProvider = "aws-provider", - AWSEnvironment = "aws-environment", - Default = "aws-default" + AWSApp = 'aws-app', + AWSResource = 'aws-resource', + AWSOrganization = 'aws-organization', + AWSProvider = 'aws-provider', + AWSEnvironment = 'aws-environment', + Default = 'aws-default', } // ---------------------------------------------------------------- AWS Apps/Resources types below ------------------------------- -/** +/** * A Generic AWS Component can be an AWS Artifact such as App, Resource, Organization * @public -*/ + */ export type AWSComponent = { componentName: string; componentType: AWSComponentType; @@ -200,13 +213,13 @@ export type AWSComponent = { componentState: ComponentStateType; repoSecretArn: string; platformRegion: string; - environments: AwsDeploymentEnvironments; // map of all the deployed environment - currentEnvironment: GenericAWSEnvironment; // selected current environment provider + environments: AwsDeploymentEnvironments; // map of all the deployed environment + currentEnvironment: GenericAWSEnvironment; // selected current environment provider setCurrentProvider: (envName: string, providerName: string) => void; getRepoInfo: () => IRepositoryInfo; -} - +}; +/** @public */ export type AWSEnvironmentProviderRecord = { id: string; name: string; @@ -215,18 +228,20 @@ export type AWSEnvironmentProviderRecord = { description: string; accountNumber: string; region: string; -} +}; +/** @public */ export enum AppStateType { - RUNNING = "Running", - STOPPED = "Stopped", - UPDATING = "Updating", - PROVISIONING = "Provisioning" + RUNNING = 'Running', + STOPPED = 'Stopped', + UPDATING = 'Updating', + PROVISIONING = 'Provisioning', } +/** @public */ export type AppState = { appID?: string; - appState?: AppStateType + appState?: AppStateType; deploymentIdentifier?: string; runningCount?: number; desiredCount?: number; @@ -234,14 +249,16 @@ export type AppState = { lastStateTimestamp?: Date; stateObject?: any; additionalInfo?: KeyValue[]; -} +}; +/** @public */ export interface KeyValue { id: string; key: string; value: string; } +/** @public */ export interface KeyValueDouble { id: string; key: string; @@ -249,4 +266,3 @@ export interface KeyValueDouble { key2: string; value2: string; } - diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/AppPromoTypes.ts b/backstage-plugins/plugins/aws-apps-common/src/types/AppPromoTypes.ts index d4955ce8..fc3a36c6 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/AppPromoTypes.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/AppPromoTypes.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +/** @public */ export type AppPromoParams = { envName: string; envRequiresManualApproval: boolean; @@ -8,6 +9,7 @@ export type AppPromoParams = { providers: AWSProviderParams[]; }; +/** @public */ export type AWSProviderParams = { awsAccount: string; awsRegion: string; @@ -16,5 +18,5 @@ export type AWSProviderParams = { envRequiresManualApproval: boolean; prefix: string; providerName: string; - parameters: { [key: string]: string } //Parameters key value map for provisioning the app on the designated provider -} + parameters: { [key: string]: string }; // Parameters key value map for provisioning the app on the designated provider +}; diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/PlatformTypes.ts b/backstage-plugins/plugins/aws-apps-common/src/types/PlatformTypes.ts index 5b832c90..fa09c121 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/PlatformTypes.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/PlatformTypes.ts @@ -1,6 +1,5 @@ -export type PlatformSCMParams = -{ - host: string - projectGroup: string; - repoName: string; -} \ No newline at end of file +export type PlatformSCMParams = { + host: string; + projectGroup: string; + repoName: string; +}; diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/SCMBackendAPI.ts b/backstage-plugins/plugins/aws-apps-common/src/types/SCMBackendAPI.ts index 5f987f57..f6931588 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/SCMBackendAPI.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/SCMBackendAPI.ts @@ -1,49 +1,53 @@ -import { GitProviders } from "./git-providers"; +import { GitProviders } from './git-providers'; -export interface IGitAPIResult -{ - isSuccuess: boolean; - message: string; - httpResponse: number; - value: any; -} +/** @public */ +export interface IGitAPIResult { + isSuccess: boolean; + message: string; + httpResponse: number; + value: any; +} +/** @public */ export interface ICommitChange { - actions: ICommitAction[]; - branch: string; - commitMessage: string; + actions: ICommitAction[]; + branch: string; + commitMessage: string; } -export interface ICommitAction -{ - action: string; - file_path: string; - content: string; +/** @public */ +export interface ICommitAction { + action: string; + file_path: string; + content: string; } +/** @public */ export enum GitVisibility { - PRIVATE = "private", - PUBIC = "public" - } + PRIVATE = 'private', + PUBIC = 'public', +} +/** @public */ export interface IRepositoryInfo { - rawIdentifier?: string; - gitHost: string; - gitProjectGroup?: string; - gitOrganization?: string; - gitRepoName: string; - gitJobID?: string; - projectID?: string; - description?: string; - isPrivate: boolean; - visibility?: GitVisibility; - gitProvider: GitProviders; + rawIdentifier?: string; + gitHost: string; + gitProjectGroup?: string; + gitOrganization?: string; + gitRepoName: string; + gitJobID?: string; + projectID?: string; + description?: string; + isPrivate: boolean; + visibility?: GitVisibility; + gitProvider: GitProviders; } // Source Control Management API +/** @public */ export interface ISCMBackendAPI { - deleteRepository: (repo: IRepositoryInfo , accessToken: string) => Promise; - createRepository: (repo: IRepositoryInfo, accessToken: string) => Promise; - getFileContent: (filePath: string, repo: IRepositoryInfo, accessToken: string) => Promise; - commitContent: (change:ICommitChange, repo: IRepositoryInfo, accessToken: string) => Promise; + deleteRepository: (repo: IRepositoryInfo, accessToken: string) => Promise; + createRepository: (repo: IRepositoryInfo, accessToken: string) => Promise; + getFileContent: (filePath: string, repo: IRepositoryInfo, accessToken: string) => Promise; + commitContent: (change: ICommitChange, repo: IRepositoryInfo, accessToken: string) => Promise; } diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/git-providers.ts b/backstage-plugins/plugins/aws-apps-common/src/types/git-providers.ts index b1eab5f8..0274b063 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/git-providers.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/git-providers.ts @@ -1,6 +1,6 @@ - +/** @public */ export enum GitProviders { - GITLAB = "gitlab", - GITHUB = "github", - UNSET = "unset" - } + GITLAB = 'gitlab', + GITHUB = 'github', + UNSET = 'unset', +} diff --git a/backstage-plugins/plugins/aws-apps-common/src/types/index.ts b/backstage-plugins/plugins/aws-apps-common/src/types/index.ts index 595ed1a3..65bcc1fd 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/types/index.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/types/index.ts @@ -4,6 +4,6 @@ export * from './AppPromoTypes'; export * from './AWSResource'; export * from './AWSServiceResources'; -export * from './AWSUIInterfaces' -export * from './git-providers' -export * from './SCMBackendAPI' \ No newline at end of file +export * from './AWSUIInterfaces'; +export * from './git-providers'; +export * from './SCMBackendAPI'; diff --git a/backstage-plugins/plugins/aws-apps-common/src/utils/git-util.ts b/backstage-plugins/plugins/aws-apps-common/src/utils/git-util.ts index 908f185c..670f373d 100644 --- a/backstage-plugins/plugins/aws-apps-common/src/utils/git-util.ts +++ b/backstage-plugins/plugins/aws-apps-common/src/utils/git-util.ts @@ -1,98 +1,82 @@ -import { GitProviders, IRepositoryInfo } from "../types" +import { GitProviders, IRepositoryInfo } from '../types'; import { Entity } from '@backstage/catalog-model'; -export const getRepoUrl= (repoInfo: IRepositoryInfo) : string => { - console.log(repoInfo); +/** @public */ +export const getRepoUrl = (repoInfo: IRepositoryInfo): string => { + // console.log(repoInfo); let gitRepoClean; - if (repoInfo.gitRepoName.includes('/')) - { - gitRepoClean = repoInfo.gitRepoName.split('/')[1] - } - else - { - gitRepoClean = repoInfo.gitRepoName + if (repoInfo.gitRepoName.includes('/')) { + gitRepoClean = repoInfo.gitRepoName.split('/')[1]; + } else { + gitRepoClean = repoInfo.gitRepoName; } - if (repoInfo.gitProvider===GitProviders.GITLAB) - { - if (repoInfo.gitProjectGroup) - { - return (repoInfo.gitHost + "/" + repoInfo.gitProjectGroup + "/" + gitRepoClean + ".git") - } - else - { - return (repoInfo.gitHost + "/" + repoInfo.gitRepoName + ".git") - } - - } - else if (repoInfo.gitProvider===GitProviders.GITHUB) - { - if (repoInfo.gitOrganization) - { - return (repoInfo.gitHost + "/" + repoInfo.gitOrganization + "/" + gitRepoClean + ".git") - } - else - { - return (repoInfo.gitHost + "/" + repoInfo.gitRepoName) + ".git" - } + if (repoInfo.gitProvider === GitProviders.GITLAB) { + if (repoInfo.gitProjectGroup) { + return `${repoInfo.gitHost}/${repoInfo.gitProjectGroup}/${gitRepoClean}.git`; } - else - { - throw Error("Unsupported git provider " + repoInfo.gitProvider); + return `${repoInfo.gitHost}/${repoInfo.gitRepoName}.git`; + } else if (repoInfo.gitProvider === GitProviders.GITHUB) { + if (repoInfo.gitOrganization) { + return `${repoInfo.gitHost}/${repoInfo.gitOrganization}/${gitRepoClean}.git`; } - + return `${repoInfo.gitHost}/${repoInfo.gitRepoName}.git`; } + throw Error(`Unsupported git provider ${repoInfo.gitProvider}`); +}; -export const getRepoInfo = (entity:Entity) : IRepositoryInfo => { - // let gitProvider: GitProviders = GitProviders.GITLAB; - let gitProvider = entity.metadata["gitProvider"] ?? GitProviders.GITLAB; - - // switch (entity.metadata["gitProvider"]){ - // case "github": - // gitProvider = GitProviders.GITHUB - // break; - // case "gitlab": - // gitProvider = GitProviders.GITLAB - // break; - // default: - // throw Error("Unsupported git provider: " + entity.metadata["gitProvider"]) - // } +/** @public */ +export const getRepoInfo = (entity: Entity): IRepositoryInfo => { + // let gitProvider: GitProviders = GitProviders.GITLAB; + const gitProvider = entity.metadata.gitProvider ?? GitProviders.GITLAB; - switch (gitProvider) { - case GitProviders.GITLAB: - return { - gitProvider, - gitHost: entity.metadata.annotations ? entity.metadata.annotations['gitlab.com/instance']?.toString() : "", - gitRepoName: entity.metadata.annotations ? entity.metadata.annotations['gitlab.com/project-slug']?.toString() : "", - gitProjectGroup: entity.metadata.annotations ? entity.metadata.annotations['gitlab.com/project-slug']?.toString().split('/')[0] : "", - isPrivate: true - } - case GitProviders.GITHUB: - return { - gitHost: "github.com", - gitRepoName: entity.metadata.annotations ? entity.metadata.annotations['github.com/project-slug']?.toString().split('/')[1] : "", - gitOrganization: entity.metadata.annotations ? entity.metadata.annotations['github.com/project-slug']?.toString().split('/')[0] : "", - gitProvider, - isPrivate: true - } - default: - throw Error("Unsupported git provider: " + entity.metadata["gitProvider"]) - } + // switch (entity.metadata["gitProvider"]){ + // case "github": + // gitProvider = GitProviders.GITHUB + // break; + // case "gitlab": + // gitProvider = GitProviders.GITLAB + // break; + // default: + // throw Error("Unsupported git provider: " + entity.metadata["gitProvider"]) + // } - }; + switch (gitProvider) { + case GitProviders.GITLAB: + return { + gitProvider, + gitHost: entity.metadata.annotations ? entity.metadata.annotations['gitlab.com/instance']?.toString() : '', + gitRepoName: entity.metadata.annotations + ? entity.metadata.annotations['gitlab.com/project-slug']?.toString() + : '', + gitProjectGroup: entity.metadata.annotations + ? entity.metadata.annotations['gitlab.com/project-slug']?.toString().split('/')[0] + : '', + isPrivate: true, + }; + case GitProviders.GITHUB: + return { + gitHost: 'github.com', + gitRepoName: entity.metadata.annotations + ? entity.metadata.annotations['github.com/project-slug']?.toString().split('/')[1] + : '', + gitOrganization: entity.metadata.annotations + ? entity.metadata.annotations['github.com/project-slug']?.toString().split('/')[0] + : '', + gitProvider, + isPrivate: true, + }; + default: + throw Error(`Unsupported git provider: ${entity.metadata.gitProvider}`); + } +}; -export const getGitCredentailsSecret= (repoInfo: IRepositoryInfo) : string => { - if (repoInfo.gitProvider===GitProviders.GITLAB) - { - return 'opa-admin-gitlab-secrets' - } - else if (repoInfo.gitProvider===GitProviders.GITHUB) - { - return 'opa-admin-github-secrets' - } - else - { - throw Error("Unsupported git provider " + repoInfo.gitProvider); - } - - } \ No newline at end of file +/** @public */ +export const getGitCredentailsSecret = (repoInfo: IRepositoryInfo): string => { + if (repoInfo.gitProvider === GitProviders.GITLAB) { + return 'opa-admin-gitlab-secrets'; + } else if (repoInfo.gitProvider === GitProviders.GITHUB) { + return 'opa-admin-github-secrets'; + } + throw Error(`Unsupported git provider ${repoInfo.gitProvider}`); +}; diff --git a/backstage-plugins/plugins/aws-apps-demo/README.md b/backstage-plugins/plugins/aws-apps-demo/README.md index 081d8736..61c4498e 100644 --- a/backstage-plugins/plugins/aws-apps-demo/README.md +++ b/backstage-plugins/plugins/aws-apps-demo/README.md @@ -1,7 +1,8 @@ - + # AWS Apps Demo AWS Apps Demo is a plugin to demonstrate branding options for a Backstage implementation. @@ -19,17 +20,17 @@ yarn add --cwd packages/app @aws/plugin-aws-apps-demo-for-backstage@0.2.0 ## Setup -1. Configure `app-config.yaml`. See [Configuration](#configuration). +1. Configure `app-config.yaml`. See [Configuration](#configuration). -2. Add the customer theme and sample home page route to the Backstage application. When adding the "<Route ...>", ensure that the "/" root path doesn't conflict with existing routes. If an existing route entry already uses "/", then update the route values across all routes to guarantee uniqueness. -**Note**: Adding a custom theme will override the default themes. If this is the first theme that you are adding to your Backstage configuration, there may be additional modifications required. See https://backstage.io/docs/getting-started/app-custom-theme/#creating-a-custom-theme for more details on additional changes to preserve existing `lightTheme` and `darkTheme` values +2. Add the customer theme and sample home page route to the Backstage application. When adding the "<Route ...>", ensure that the "/" root path doesn't conflict with existing routes. If an existing route entry already uses "/", then update the route values across all routes to guarantee uniqueness. + **Note**: Adding a custom theme will override the default themes. If this is the first theme that you are adding to your Backstage configuration, there may be additional modifications required. See https://backstage.io/docs/getting-started/app-custom-theme/#creating-a-custom-theme for more details on additional changes to preserve existing `lightTheme` and `darkTheme` values ```diff // packages/app/src/App.tsx + import { OPAHomePage, customerTheme } from '@aws/plugin-aws-apps-demo-for-backstage'; + import { darkTheme, lightTheme } from '@backstage/theme'; - + const app = createApp({ ... themes: [ @@ -67,7 +68,7 @@ yarn add --cwd packages/app @aws/plugin-aws-apps-demo-for-backstage@0.2.0 ... ] }) - + const routes = ( - } /> @@ -130,10 +131,10 @@ yarn add --cwd packages/app @aws/plugin-aws-apps-demo-for-backstage@0.2.0 ## Configuration -AWS Apps Demo has two configuration fields. Each of these fields are optional. If they are not specified, then default values will be used. +AWS Apps Demo has two configuration fields. Each of these fields are optional. If they are not specified, then default values will be used. -- `logo` - a URL pointing to an image file to be used on the home page and in the SideBar when it is expanded. The default value will point to an Amazon logo. -- `logoIcon` - a URL pointing to an image file to be used in the SideBar when it is collapsed. When the sidebar is collapsed, there is limited space, so this image should be a small image which is roughly square in shape. The default value will point to a small Amazon logo. +- `logo` - a URL pointing to an image file to be used on the home page and in the SideBar when it is expanded. The default value will point to an Amazon logo. +- `logoIcon` - a URL pointing to an image file to be used in the SideBar when it is collapsed. When the sidebar is collapsed, there is limited space, so this image should be a small image which is roughly square in shape. The default value will point to a small Amazon logo. Note that you may also need to configure the **backend.csp.img-src** value to allow for loading images from a remote site. @@ -156,7 +157,8 @@ backend: ## Next Steps -The home page provided in this plugin is a simple demonstration page. You can modify the `OPAHomePage.tsx` content to meet your needs. See the Backstage documentation for home page setup and customization for more details on how you can customize your experience: https://backstage.io/docs/getting-started/homepage +The home page provided in this plugin is a simple demonstration page. You can modify the `OPAHomePage.tsx` content to meet your needs. See the Backstage documentation for home page setup and customization for more details on how you can customize your experience: https://backstage.io/docs/getting-started/homepage + [homepageImage]: docs/images/homePage.png 'AWS Apps Demo Home page' diff --git a/backstage-plugins/plugins/aws-apps-demo/package.json b/backstage-plugins/plugins/aws-apps-demo/package.json index 4badcc66..3437d291 100644 --- a/backstage-plugins/plugins/aws-apps-demo/package.json +++ b/backstage-plugins/plugins/aws-apps-demo/package.json @@ -26,9 +26,10 @@ "role": "frontend-plugin", "pluginId": "aws-apps-demo", "pluginPackages": [ - "@backstage/plugin-home" + "@aws/plugin-aws-apps-demo-for-backstage" ] }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -42,7 +43,7 @@ "@backstage/core-components": "^0.14.9", "@backstage/core-plugin-api": "^1.9.3", "@backstage/plugin-catalog-react": "^1.12.2", - "@backstage/plugin-home": "^0.7.8", + "@backstage/plugin-home": "^0.7.7", "@backstage/plugin-search": "^1.4.14", "@backstage/plugin-techdocs-react": "^1.2.6", "@backstage/theme": "^0.5.6", @@ -56,9 +57,9 @@ }, "devDependencies": { "@backstage/cli": "^0.26.11", - "@backstage/core-app-api": "^1.14.1", - "@backstage/dev-utils": "^1.0.36", - "@backstage/test-utils": "^1.5.9", + "@backstage/core-app-api": "^1.14.0", + "@backstage/dev-utils": "^1.0.35", + "@backstage/test-utils": "^1.5.8", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/AWSAppsHomePage/AWSAppsHomePage.tsx b/backstage-plugins/plugins/aws-apps-demo/src/components/AWSAppsHomePage/AWSAppsHomePage.tsx index 75aae690..b405e5ac 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/AWSAppsHomePage/AWSAppsHomePage.tsx +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/AWSAppsHomePage/AWSAppsHomePage.tsx @@ -59,10 +59,11 @@ export const AWSAppsHomePage = () => {
} pageTitleOverride="Home" /> - + placeholder="Search" + /> diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/OPAHomePage/OPAHomePage.tsx b/backstage-plugins/plugins/aws-apps-demo/src/components/OPAHomePage/OPAHomePage.tsx index c5ad3b99..6a6c1a19 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/OPAHomePage/OPAHomePage.tsx +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/OPAHomePage/OPAHomePage.tsx @@ -32,7 +32,6 @@ const useStyles = makeStyles(theme => ({ }, })); - const useLogoStyles = makeStyles(theme => ({ container: { margin: theme.spacing(2, 0), @@ -54,7 +53,6 @@ export const OPAHomePage = () => { - } /> @@ -66,10 +64,11 @@ export const OPAHomePage = () => {
} pageTitleOverride="Home" /> - + placeholder="Search" + /> diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/OPALogoFull.tsx b/backstage-plugins/plugins/aws-apps-demo/src/components/OPALogoFull.tsx index 5ea4b795..7f62f93f 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/OPALogoFull.tsx +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/OPALogoFull.tsx @@ -32,7 +32,7 @@ const useStyles = makeStyles({ strokeMiterlimit: 10, strokeDasharray: 'none', strokeOpacity: 1, - } + }, }); export const OPALogoFull = () => { @@ -47,105 +47,79 @@ export const OPALogoFull = () => { viewBox="0 0 2729.3333 2093.3332" xmlnsXlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" - className={classes.svg}> - - - + className={classes.svg} + > + + + - - + + - - + + - - + + - - + + - - + + - + + transform="matrix(1.3333333,0,0,1.3333333,23627.94,1405.5287)" + /> + clip-path="url(#clipPath114)" + /> + clip-path="url(#clipPath116)" + /> + clip-path="url(#clipPath118)" + /> + clip-path="url(#clipPath120)" + /> + clip-path="url(#clipPath122)" + /> + clip-path="url(#clipPath124)" + /> - ); }; diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/aws-theme.ts b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/aws-theme.ts index e225a24c..a9383f95 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/aws-theme.ts +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/aws-theme.ts @@ -82,9 +82,7 @@ const baseTheme = createTheme({ defaultPageTheme: 'home', }); -const createCustomThemeOverrides = ( - theme: BackstageTheme, -): BackstageOverrides & CatalogReactOverrides => { +const createCustomThemeOverrides = (theme: BackstageTheme): BackstageOverrides & CatalogReactOverrides => { return { BackstageHeader: { header: { @@ -109,7 +107,7 @@ const createCustomThemeOverrides = ( }, value: { color: 'rgba(0, 0, 0, 0.54)', - } + }, }, BackstageHeaderTabs: { defaultTab: { diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/customer-theme.ts b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/customer-theme.ts index d3acb123..7482905a 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/customer-theme.ts +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/customer-theme.ts @@ -4,7 +4,7 @@ import { BackstageOverrides } from '@backstage/core-components'; import { BackstageOverrides as CatalogReactOverrides } from '@backstage/plugin-catalog-react'; import { BackstageTheme, createTheme, lightTheme } from '@backstage/theme'; -import { Overrides as _overrides } from "@material-ui/core/styles/overrides"; +import { Overrides as _overrides } from '@material-ui/core/styles/overrides'; import { AutocompleteClassKey } from '@material-ui/lab/Autocomplete'; import { AlertClassKey } from '@material-ui/lab/Alert'; diff --git a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/opa-theme.ts b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/opa-theme.ts index 7f2455f0..0ac82e11 100644 --- a/backstage-plugins/plugins/aws-apps-demo/src/components/theme/opa-theme.ts +++ b/backstage-plugins/plugins/aws-apps-demo/src/components/theme/opa-theme.ts @@ -82,9 +82,7 @@ const baseTheme = createTheme({ defaultPageTheme: 'home', }); -const createCustomThemeOverrides = ( - theme: BackstageTheme, -): BackstageOverrides & CatalogReactOverrides => { +const createCustomThemeOverrides = (theme: BackstageTheme): BackstageOverrides & CatalogReactOverrides => { return { BackstageHeader: { header: { @@ -109,7 +107,7 @@ const createCustomThemeOverrides = ( }, value: { color: 'rgba(0, 0, 0, 0.54)', - } + }, }, BackstageHeaderTabs: { defaultTab: { diff --git a/backstage-plugins/plugins/aws-apps/README.md b/backstage-plugins/plugins/aws-apps/README.md index adb4e428..9156f03e 100644 --- a/backstage-plugins/plugins/aws-apps/README.md +++ b/backstage-plugins/plugins/aws-apps/README.md @@ -1,10 +1,11 @@ - + # OPA on AWS Frontend -This is the frontend UI of the OPA on AWS plugin. An AWS Catalog Page and several entity cards are contributed to the UI from this plugin. +This is the frontend UI of the OPA on AWS plugin. An AWS Catalog Page and several entity cards are contributed to the UI from this plugin. - [Installation](#installation) - [Configuration](#configuration) @@ -23,13 +24,14 @@ yarn add --cwd packages/app @aws/plugin-aws-apps-for-backstage@0.2.0 ## Configuration -Each of the UI components contributed in the OPA on AWS frontend plugin can be configured independently and added to your Backstage platform as desired. Details for adding each type of UI component are found in the sections below. +Each of the UI components contributed in the OPA on AWS frontend plugin can be configured independently and added to your Backstage platform as desired. Details for adding each type of UI component are found in the sections below. ### EntityPage customization for AWS apps To build an AWS app-specific entity presentation, we will rely on identification of a component as being of type "aws-app" (as specified under the `spec.type` configuration in the entity's `catalog-info.yaml` file). Add the code shown below to `EntityPage.tsx` + ```ts // packages/app/src/components/catalog/EntityPage.tsx @@ -117,9 +119,10 @@ const entityPage = ( ); ``` + When running the Backstage app, you are now setup for customized views of AWS applications in the platform. -### AWS Software Catalog Page +### AWS Software Catalog Page The AWS Software Catalog page provides a customized view into the Backstage catalog with a focus on applications deployed to AWS through Backstage. ![AWS Software Catalog Page](images/ui_aws_software_catalog.png 'AWS Software Catalog Page') @@ -159,7 +162,7 @@ const routes = ( ``` Next, add the AWS Software Catalog to the sidebar navigation in the `Root.tsx` file. -Determine your preferred placement in the sidebar using the example below as guidance. Exact contents and children of the may differ in your installation. +Determine your preferred placement in the sidebar using the example below as guidance. Exact contents and children of the may differ in your installation. ```diff // packages/app/src/components/Root/Root.tsx diff --git a/backstage-plugins/plugins/aws-apps/package.json b/backstage-plugins/plugins/aws-apps/package.json index ca05c0fe..485e9d8c 100644 --- a/backstage-plugins/plugins/aws-apps/package.json +++ b/backstage-plugins/plugins/aws-apps/package.json @@ -25,8 +25,11 @@ "backstage": { "role": "frontend-plugin", "pluginId": "aws-apps", - "pluginPackages": ["@backstage/plugin-catalog"] + "pluginPackages": [ + "@aws/plugin-aws-apps-for-backstage" + ] }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -42,24 +45,25 @@ "@aws-sdk/client-dynamodb": "^3.623.0", "@aws-sdk/client-ecs": "^3.623.0", "@aws-sdk/client-eks": "^3.623.0", - "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/client-lambda": "^3.623.0", + "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/client-secrets-manager": "^3.623.0", "@aws-sdk/client-ssm": "^3.623.0", "@aws-sdk/util-arn-parser": "^3.568.0", - "@aws/plugin-aws-apps-common-for-backstage": "^0.3.4", "@aws/backstage-plugin-catalog-backend-module-aws-apps-entities-processor": "^0.3.4", - "@immobiliarelabs/backstage-plugin-gitlab": "^6.6.0", + "@aws/plugin-aws-apps-common-for-backstage": "^0.3.4", "@backstage/catalog-model": "^1.5.0", "@backstage/core-components": "^0.14.9", "@backstage/core-plugin-api": "^1.9.3", "@backstage/errors": "^1.2.4", "@backstage/plugin-catalog": "^1.21.1", "@backstage/plugin-catalog-react": "^1.12.2", + "@backstage/plugin-github-actions": "^0.6.16", "@backstage/plugin-permission-react": "^0.4.24", "@backstage/theme": "^0.5.6", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", + "@immobiliarelabs/backstage-plugin-gitlab": "^6.6.0", "@kubernetes/client-node": "^0.21.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -75,9 +79,9 @@ }, "devDependencies": { "@backstage/cli": "^0.26.11", - "@backstage/core-app-api": "^1.14.1", - "@backstage/dev-utils": "^1.0.36", - "@backstage/test-utils": "^1.5.9", + "@backstage/core-app-api": "^1.14.0", + "@backstage/dev-utils": "^1.0.35", + "@backstage/test-utils": "^1.5.8", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", diff --git a/backstage-plugins/plugins/aws-apps/src/api/OPAApi.ts b/backstage-plugins/plugins/aws-apps/src/api/OPAApi.ts index 6eee06f6..c1ff8fb9 100644 --- a/backstage-plugins/plugins/aws-apps/src/api/OPAApi.ts +++ b/backstage-plugins/plugins/aws-apps/src/api/OPAApi.ts @@ -6,26 +6,31 @@ import { DeleteStackCommandOutput, DescribeStackEventsCommandOutput, Stack, - UpdateStackCommandOutput -} from "@aws-sdk/client-cloudformation"; + UpdateStackCommandOutput, +} from '@aws-sdk/client-cloudformation'; import { LogStream } from '@aws-sdk/client-cloudwatch-logs'; import { ScanCommandOutput } from '@aws-sdk/client-dynamodb'; import { Service, Task, TaskDefinition } from '@aws-sdk/client-ecs'; import { HeadObjectCommandOutput } from '@aws-sdk/client-s3'; import { DeleteSecretCommandOutput, GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; import { GetParameterCommandOutput } from '@aws-sdk/client-ssm'; -import { AWSProviderParams, AWSServiceResources, BackendParams, BindResourceParams, AWSEnvironmentProviderRecord } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSProviderParams, + AWSServiceResources, + BackendParams, + BindResourceParams, + AWSEnvironmentProviderRecord, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { createApiRef } from '@backstage/core-plugin-api'; import { ContainerDetailsType } from '../types'; -import { InvokeCommandOutput } from "@aws-sdk/client-lambda"; -import { IRepositoryInfo } from "@aws/plugin-aws-apps-common-for-backstage"; +import { InvokeCommandOutput } from '@aws-sdk/client-lambda'; +import { IRepositoryInfo } from '@aws/plugin-aws-apps-common-for-backstage'; export const opaApiRef = createApiRef({ id: 'plugin.opa.app', }); export interface OPAApi { - setPlatformParams(appName: string, region: string): void; setBackendParams(backendParams: BackendParams): void; @@ -38,7 +43,7 @@ export interface OPAApi { }: { service: string; cluster: string; - backendParamsOverrides?: BackendParams + backendParamsOverrides?: BackendParams; }): Promise; updateService({ @@ -65,17 +70,9 @@ export interface OPAApi { backendParamsOverrides?: BackendParams; }): Promise; - getPlatformSecret({ - secretName, - }: { - secretName: string; - }): Promise; + getPlatformSecret({ secretName }: { secretName: string }): Promise; - getPlatformSSMParam({ - paramName, - }: { - paramName: string; - }): Promise; + getPlatformSSMParam({ paramName }: { paramName: string }): Promise; bindResource({ repoInfo, @@ -129,11 +126,7 @@ export interface OPAApi { envName: string; }): Promise; - deletePlatformSecret({ - secretName - }: { - secretName: string; - }): Promise; + deletePlatformSecret({ secretName }: { secretName: string }): Promise; deleteRepository({ repoInfo, @@ -291,7 +284,7 @@ export interface OPAApi { functionName, actionDescription, body, - backendParamsOverrides + backendParamsOverrides, }: { functionName: string; actionDescription: string; @@ -303,13 +296,13 @@ export interface OPAApi { envName, gitAdminSecret, repoInfo, - backendParamsOverrides + backendParamsOverrides, }: { envName: string; gitAdminSecret: string; repoInfo: IRepositoryInfo; backendParamsOverrides?: BackendParams; - }): Promise + }): Promise; updateEKSApp({ actionDescription, @@ -321,7 +314,7 @@ export interface OPAApi { lambdaRoleArn, gitAdminSecret, repoInfo, - backendParamsOverrides + backendParamsOverrides, }: { actionDescription: string; envName: string; diff --git a/backstage-plugins/plugins/aws-apps/src/api/OPAApiClient.ts b/backstage-plugins/plugins/aws-apps/src/api/OPAApiClient.ts index 001e5d0a..84f94fef 100644 --- a/backstage-plugins/plugins/aws-apps/src/api/OPAApiClient.ts +++ b/backstage-plugins/plugins/aws-apps/src/api/OPAApiClient.ts @@ -6,22 +6,28 @@ import { DeleteStackCommandOutput, DescribeStackEventsCommandOutput, Stack, - UpdateStackCommandOutput -} from "@aws-sdk/client-cloudformation"; + UpdateStackCommandOutput, +} from '@aws-sdk/client-cloudformation'; import { LogStream } from '@aws-sdk/client-cloudwatch-logs'; import { ScanCommandOutput } from '@aws-sdk/client-dynamodb'; import { Service, Task, TaskDefinition } from '@aws-sdk/client-ecs'; import { HeadObjectCommandOutput } from '@aws-sdk/client-s3'; import { DeleteSecretCommandOutput, GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; import { GetParameterCommandOutput } from '@aws-sdk/client-ssm'; -import { AWSEnvironmentProviderRecord, AWSProviderParams, AWSServiceResources, BackendParams, BindResourceParams, IRepositoryInfo } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSEnvironmentProviderRecord, + AWSProviderParams, + AWSServiceResources, + BackendParams, + BindResourceParams, + IRepositoryInfo, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { ConfigApi, FetchApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { OPAApi } from '.'; import { HTTP } from '../helpers/constants'; import { ContainerDetailsType } from '../types'; -import { InvokeCommandOutput } from "@aws-sdk/client-lambda"; - +import { InvokeCommandOutput } from '@aws-sdk/client-lambda'; export class OPAApiClient implements OPAApi { private readonly configApi: ConfigApi; @@ -30,7 +36,7 @@ export class OPAApiClient implements OPAApi { private platformAppName: string; private platformRegion: string; - public constructor(options: { configApi: ConfigApi; fetchApi: FetchApi; }) { + public constructor(options: { configApi: ConfigApi; fetchApi: FetchApi }) { this.configApi = options.configApi; this.fetchApi = options.fetchApi; this.backendParams = { appName: '', awsAccount: '', awsRegion: '', prefix: '', providerName: '' }; @@ -62,16 +68,15 @@ export class OPAApiClient implements OPAApi { }: { cluster: string; service: string; - backendParamsOverrides?: BackendParams + backendParamsOverrides?: BackendParams; }): Promise { - const beParams = this.getAppliedBackendParams(backendParamsOverrides); const postBody = { ...beParams, serviceName: service, clusterName: cluster, - } + }; const task = this.fetch('/ecs', HTTP.POST, postBody); @@ -99,9 +104,8 @@ export class OPAApiClient implements OPAApi { taskDefinition: string; desiredCount: number | undefined; restart: boolean; - backendParamsOverrides?: BackendParams + backendParamsOverrides?: BackendParams; }): Promise { - const beParams = this.getAppliedBackendParams(backendParamsOverrides); const postBody = { @@ -111,7 +115,7 @@ export class OPAApiClient implements OPAApi { taskDefinition: taskDefinition, restart: restart, desiredCount: desiredCount, - } + }; const serviceDetails = this.fetch('/ecs/updateService', HTTP.POST, postBody); @@ -130,7 +134,7 @@ export class OPAApiClient implements OPAApi { const postBody = { ...beParams, secretArn: secretName, - } + }; const secretDetails = this.fetch('/secrets', HTTP.POST, postBody); return secretDetails; @@ -161,7 +165,7 @@ export class OPAApiClient implements OPAApi { }: { logGroupName: string; logStreamName: string; - backendParamsOverrides?: BackendParams + backendParamsOverrides?: BackendParams; }): Promise { const beParams = this.getAppliedBackendParams(backendParamsOverrides); const path = '/logs/stream-events'; @@ -176,33 +180,22 @@ export class OPAApiClient implements OPAApi { } //TODO: Move platform calls to separate backend endpoints - interface does not require provider information - async getPlatformSecret({ - secretName, - }: { - secretName: string; - }): Promise { - + async getPlatformSecret({ secretName }: { secretName: string }): Promise { const postBody = { ...this.backendParams, - secretArn: secretName - } + secretArn: secretName, + }; const secretDetails = this.fetch('/platform/secrets', HTTP.POST, postBody); return secretDetails; } - async getPlatformSSMParam({ - paramName, - }: { - paramName: string; - }): Promise { - - + async getPlatformSSMParam({ paramName }: { paramName: string }): Promise { const postBody = { ...this.backendParams, - paramName - } + paramName, + }; const paramDetails = this.fetch('/platform/ssm', HTTP.POST, postBody); @@ -217,10 +210,8 @@ export class OPAApiClient implements OPAApi { repoInfo: IRepositoryInfo; params: BindResourceParams; gitAdminSecret: string; - backendParamsOverrides?: BackendParams + backendParamsOverrides?: BackendParams; }): Promise { - - const postBody = { ...this.backendParams, providerName: params.providerName, @@ -229,8 +220,8 @@ export class OPAApiClient implements OPAApi { envName: params.envName, policies: params.policies, resourceName: params.resourceName, - resourceEntityRef: params.resourceEntityRef - } + resourceEntityRef: params.resourceEntityRef, + }; const bindResponse = this.fetch('/platform/bind-resource', HTTP.POST, postBody); @@ -259,8 +250,8 @@ export class OPAApiClient implements OPAApi { envName: params.envName, policies: params.policies, resourceName: params.resourceName, - resourceEntityRef: params.resourceEntityRef - } + resourceEntityRef: params.resourceEntityRef, + }; const unBindResponse = this.fetch('/platform/unbind-resource', HTTP.POST, postBody); @@ -276,7 +267,7 @@ export class OPAApiClient implements OPAApi { backendParamsOverrides, }: { repoInfo: IRepositoryInfo; - provider: AWSEnvironmentProviderRecord + provider: AWSEnvironmentProviderRecord; gitAdminSecret: string; envName: string; action: string; @@ -289,11 +280,11 @@ export class OPAApiClient implements OPAApi { repoInfo, gitAdminSecret, envName, - action - } + action, + }; const addProviderResponse = this.fetch('/platform/update-provider', HTTP.POST, postBody); - return addProviderResponse + return addProviderResponse; } deleteTFProvider({ @@ -303,7 +294,7 @@ export class OPAApiClient implements OPAApi { envName, }: { backendParamsOverrides: BackendParams; - repoInfo: IRepositoryInfo + repoInfo: IRepositoryInfo; gitAdminSecret: string; envName: string; }): Promise { @@ -312,33 +303,28 @@ export class OPAApiClient implements OPAApi { ...beParams, repoInfo, gitAdminSecret, - envName - } + envName, + }; const deleteResponse = this.fetch('/platform/delete-tf-provider', HTTP.POST, postBody); return deleteResponse; } - async deletePlatformSecret({ - secretName - }: { - secretName: string; - }): Promise { + async deletePlatformSecret({ secretName }: { secretName: string }): Promise { const postBody = { awsRegion: this.platformRegion, awsAccount: '', appName: this.platformAppName, prefix: '', providerName: '', - secretName - } + secretName, + }; const deleteResponse = this.fetch('/platform/delete-secret', HTTP.POST, postBody); return deleteResponse; - - }; + } async deleteRepository({ repoInfo, @@ -353,8 +339,8 @@ export class OPAApiClient implements OPAApi { const postBody = { ...beParams, repoInfo, - gitAdminSecret - } + gitAdminSecret, + }; const deleteResponse = this.fetch('/platform/delete-repository', HTTP.POST, postBody); return deleteResponse; } @@ -377,9 +363,13 @@ export class OPAApiClient implements OPAApi { return stack; } - async getStackEvents( - { stackName, backendParamsOverrides, }: { stackName: string; backendParamsOverrides?: BackendParams; }) - : Promise { + async getStackEvents({ + stackName, + backendParamsOverrides, + }: { + stackName: string; + backendParamsOverrides?: BackendParams; + }): Promise { const beParams = this.getAppliedBackendParams(backendParamsOverrides); const path = '/cloudformation/describeStackEvents'; @@ -550,7 +540,7 @@ export class OPAApiClient implements OPAApi { const postBody = { ...beParams, taskDefinition: taskDefinitionArn, - } + }; const taskD = this.fetch('/ecs/describeTaskDefinition', HTTP.POST, postBody); return taskD; @@ -569,8 +559,8 @@ export class OPAApiClient implements OPAApi { const postBody = { ...beParams, taskDefinition: taskDefinitionArn, - envVar: envVar - } + envVar: envVar, + }; const taskD = this.fetch('/ecs/updateTaskDefinition', HTTP.POST, postBody); return taskD; @@ -597,8 +587,8 @@ export class OPAApiClient implements OPAApi { envRequiresManualApproval, repoInfo, gitAdminSecret, - providers: providersData - } + providers: providersData, + }; const results = this.fetch('/git/promote', HTTP.POST, postBody); return results; } @@ -607,7 +597,7 @@ export class OPAApiClient implements OPAApi { functionName, actionDescription, body, - backendParamsOverrides + backendParamsOverrides, }: { functionName: string; actionDescription: string; @@ -619,7 +609,7 @@ export class OPAApiClient implements OPAApi { ...beParams, functionName, actionDescription, - body + body, }; // console.log(postBody) const lambdaResult = await this.fetch('/lambda/invoke', HTTP.POST, postBody); @@ -630,7 +620,7 @@ export class OPAApiClient implements OPAApi { envName, gitAdminSecret, repoInfo, - backendParamsOverrides + backendParamsOverrides, }: { envName: string; gitAdminSecret: string; @@ -643,7 +633,7 @@ export class OPAApiClient implements OPAApi { ...beParams, envName, gitAdminSecret, - repoInfo + repoInfo, }; // console.log(postBody) @@ -663,7 +653,7 @@ export class OPAApiClient implements OPAApi { lambdaRoleArn, gitAdminSecret, repoInfo, - backendParamsOverrides + backendParamsOverrides, }: { actionDescription: string; envName: string; @@ -680,7 +670,7 @@ export class OPAApiClient implements OPAApi { envName, gitAdminSecret, repoInfo, - backendParamsOverrides + backendParamsOverrides, }); const updateKeyArray = updateKey.split('.'); @@ -693,39 +683,37 @@ export class OPAApiClient implements OPAApi { // console.log(currObj) if (i === updateKeyArray.length - 1) { // console.log(`key ${currKey} found , current value ${currObj[currKey]}`) - currObj[currKey] = updateValue + currObj[currKey] = updateValue; // console.log(`Updating key ${currKey} found , current value ${currObj[currKey]}`) - } else { - currObj = currObj[currKey] + currObj = currObj[currKey]; } - } - else { - break + } else { + break; } } - }) + }); // console.log(configResult) //make changes to config - const manifest = JSON.stringify(configResult) + const manifest = JSON.stringify(configResult); const bodyParam = { - RequestType: "Update", - ResourceType: "Custom::AWSCDK-EKS-KubernetesResource", + RequestType: 'Update', + ResourceType: 'Custom::AWSCDK-EKS-KubernetesResource', ResourceProperties: { - TimeoutSeconds: "5", + TimeoutSeconds: '5', ClusterName: cluster, RoleArn: lambdaRoleArn, InvocationType: 'RequestResponse', Manifest: manifest, - } + }, }; const configUpdateResult = await this.invokeLambda({ functionName: kubectlLambda, actionDescription, - body: JSON.stringify(bodyParam) - }) + body: JSON.stringify(bodyParam), + }); return configUpdateResult; } diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AdvancedEntityTypePicker.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AdvancedEntityTypePicker.tsx index 860026f1..ea569011 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AdvancedEntityTypePicker.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AdvancedEntityTypePicker.tsx @@ -23,8 +23,12 @@ function filterTypes(allTypes: string[], allowedTypes?: string[]): Record { - if (a < b) { return -1; } - if (a > b) { return 1; } + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } return 0; }); const typesMap = availableTypes.reduce((acc, kind) => { diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AppCatalogPage.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AppCatalogPage.tsx index cd5db3f0..26285efe 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AppCatalogPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/AppCatalogPage.tsx @@ -112,10 +112,9 @@ export function AppCatalogPage(props: AppCatalogPageProps) { columnFactories.createMetadataDescriptionColumn(), columnFactories.createTagsColumn(), ]; - columns=awsAppsColumns + columns = awsAppsColumns; allowedKinds = ['Component']; initiallySelectedFilter = 'all'; - } else if (kind === 'resource') { const awsResourcesColumns: TableColumn[] = [ columnFactories.createTitleColumn({ hidden: true }), diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/awsColumns.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/awsColumns.tsx index 4e3f8df8..d29293bf 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/awsColumns.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppCatalogPage/awsColumns.tsx @@ -2,11 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { - humanizeEntityRef, - EntityRefLink, - EntityRefLinks, -} from '@backstage/plugin-catalog-react'; +import { humanizeEntityRef, EntityRefLink, EntityRefLinks } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; @@ -18,9 +14,7 @@ import { CatalogTableRow } from '@backstage/plugin-catalog'; // CatalogTable.columns field. /** @public */ export const columnFactories = Object.freeze({ - createNameColumn(options?: { - defaultKind?: string; - }): TableColumn { + createNameColumn(options?: { defaultKind?: string }): TableColumn { function formatContent(entity: Entity): string { return ( entity.metadata?.title || @@ -40,28 +34,17 @@ export const columnFactories = Object.freeze({ return formatContent(entity1).localeCompare(formatContent(entity2)); }, cellStyle: { - minWidth:'175px' + minWidth: '175px', }, - render: ({ entity }) => ( - - ), - + render: ({ entity }) => , }; }, createSystemColumn(): TableColumn { return { title: 'System', field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - width:'auto' + render: ({ resolved }) => , + width: 'auto', }; }, createOwnerColumn(): TableColumn { @@ -69,14 +52,9 @@ export const columnFactories = Object.freeze({ title: 'Owner', field: 'resolved.ownedByRelationsTitle', cellStyle: { - minWidth:'130px' + minWidth: '130px', }, - render: ({ resolved }) => ( - - ), + render: ({ resolved }) => , }; }, createSpecTargetsColumn(): TableColumn { @@ -87,9 +65,7 @@ export const columnFactories = Object.freeze({ <> {(entity?.spec?.targets || entity?.spec?.target) && ( )} @@ -112,7 +88,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - width:'auto' + width: 'auto', }; }, createMetadataDescriptionColumn(): TableColumn { @@ -120,15 +96,9 @@ export const columnFactories = Object.freeze({ title: 'Description', field: 'entity.metadata.description', cellStyle: { - minWidth:'175px' + minWidth: '175px', }, - render: ({ entity }) => ( - - ), - + render: ({ entity }) => , }; }, createProviderAccountColumn(): TableColumn { @@ -137,15 +107,9 @@ export const columnFactories = Object.freeze({ field: 'entity.metadata["awsAccount"]', cellStyle: { padding: '0px 16px 0px 20px', - minWidth:'150px' + minWidth: '150px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["awsAccount"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['awsAccount']?.toString() || ''}, width: 'auto', }; }, @@ -156,13 +120,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["awsRegion"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['awsRegion']?.toString() || ''}, width: 'auto', }; }, @@ -173,13 +131,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["prefix"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['prefix']?.toString() || ''}, width: 'auto', }; }, @@ -189,17 +141,10 @@ export const columnFactories = Object.freeze({ field: 'entity.metadata["resourceType"]', cellStyle: { padding: '0px 16px 0px 20px', - minWidth:'30px' + minWidth: '30px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["resourceType"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['resourceType']?.toString() || ''}, //width: 'auto', - }; }, createIACColumn(): TableColumn { @@ -209,13 +154,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["iacType"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['iacType']?.toString() || ''}, width: 'auto', }; }, @@ -226,13 +165,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["environmentType"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['environmentType']?.toString() || ''}, width: 'auto', }; }, @@ -243,13 +176,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.spec?.["system"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.spec?.['system']?.toString() || ''}, width: 'auto', }; }, @@ -260,13 +187,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["category"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['category']?.toString() || ''}, width: 'auto', }; }, @@ -277,13 +198,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["classification"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['classification']?.toString() || ''}, width: 'auto', }; }, @@ -294,13 +209,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["level"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['level']?.toString() || ''}, width: 'auto', }; }, @@ -311,13 +220,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["envTypeAccount"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['envTypeAccount']?.toString() || ''}, width: 'auto', }; }, @@ -328,13 +231,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["envTypeRegion"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['envTypeRegion']?.toString() || ''}, width: 'auto', }; }, @@ -345,13 +242,7 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }) => ( - <> - { - entity.metadata["envType"]?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.metadata['envType']?.toString() || ''}, width: 'auto', }; }, @@ -361,15 +252,9 @@ export const columnFactories = Object.freeze({ field: 'entity.spec.subType', cellStyle: { padding: '0px 16px 0px 20px', - minWidth:'150px' + minWidth: '150px', }, - render: ({ entity }) => ( - <> - { - entity.spec?.subType?.toString() || "" - } - - ), + render: ({ entity }) => <>{entity.spec?.subType?.toString() || ''}, width: 'auto', }; }, @@ -384,22 +269,14 @@ export const columnFactories = Object.freeze({ <> {entity.metadata.tags && entity.metadata.tags.map(t => ( - + ))} ), width: 'auto', }; }, - createTitleColumn(options?: { - hidden?: boolean; - }): TableColumn { + createTitleColumn(options?: { hidden?: boolean }): TableColumn { return { title: 'Title', field: 'entity.metadata.title', @@ -407,10 +284,7 @@ export const columnFactories = Object.freeze({ searchable: true, }; }, - createLabelColumn( - key: string, - options?: { title?: string; defaultValue?: string }, - ): TableColumn { + createLabelColumn(key: string, options?: { title?: string; defaultValue?: string }): TableColumn { return { title: options?.title ?? 'Label', field: 'entity.metadata.labels', @@ -418,19 +292,12 @@ export const columnFactories = Object.freeze({ padding: '0px 16px 0px 20px', }, render: ({ entity }: { entity: Entity }) => { - const labels: Record | undefined = - entity.metadata?.labels; - const specifiedLabelValue = - (labels && labels[key]) || options?.defaultValue; + const labels: Record | undefined = entity.metadata?.labels; + const specifiedLabelValue = (labels && labels[key]) || options?.defaultValue; return ( <> {specifiedLabelValue && ( - + )} ); diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppConfigCard/AppConfigCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppConfigCard/AppConfigCard.tsx index 7a5dc96e..ac4a588f 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppConfigCard/AppConfigCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppConfigCard/AppConfigCard.tsx @@ -29,7 +29,7 @@ const AppConfigOverview = ({ const [error, setError] = useState<{ isError: boolean; errorMsg: string | null }>({ isError: false, errorMsg: null }); const env = awsComponent.currentEnvironment as AWSECSAppDeploymentEnvironment; // get latest task definition - const latestTaskDef = env.app.taskDefArn.substring(0, env.app.taskDefArn.lastIndexOf(":")) + const latestTaskDef = env.app.taskDefArn.substring(0, env.app.taskDefArn.lastIndexOf(':')); async function getData() { const taskDefinition = await api.describeTaskDefinition({ @@ -60,7 +60,6 @@ const AppConfigOverview = ({ }, []); const onEdit = (containerName: string) => { - // don't allow switching out of edit mode if any environment variables are empty if (edit) { let emptyVar = false; @@ -101,7 +100,7 @@ const AppConfigOverview = ({ api .updateTaskDefinition({ taskDefinitionArn: latestTaskDef, - envVar: envVariables + envVar: envVariables, }) .then(td => { const containerDet = td.containerDefinitions?.map(condef => { @@ -135,7 +134,6 @@ const AppConfigOverview = ({ setLoading(false); setError({ isError: true, errorMsg: `Unexpected error occurred while udpating taskDefinition: ${e}` }); }); - }; // Returns a new object reference that is a shallow clone of envVariables, except for the @@ -153,7 +151,6 @@ const AppConfigOverview = ({ }; const checkForUnsavedChanges = (containerName: string, newDetails: ContainerDetailsType[]) => { - if (savedEnvVariables === newDetails) { setUnsavedChanges(false); return; @@ -177,16 +174,14 @@ const AppConfigOverview = ({ for (let index = 0; index < (savedDetails.env?.length || 0); index++) { const keyValPair = savedDetails.env![index]; - if (keyValPair?.name !== details.env?.[index]?.name || - keyValPair?.value !== details.env?.[index]?.value) { - + if (keyValPair?.name !== details.env?.[index]?.name || keyValPair?.value !== details.env?.[index]?.value) { setUnsavedChanges(true); return; } } setUnsavedChanges(false); - } + }; const onEnvVarChange = (containerName: string, type: string, value: string, envVarIndex: number) => { const newState = getEnvVarsPartialDeepClone(containerName); @@ -194,7 +189,7 @@ const AppConfigOverview = ({ const originalKeyVal = containerDetails.env![envVarIndex]; - if (type === "key") { + if (type === 'key') { containerDetails.env![envVarIndex] = { ...originalKeyVal, name: value }; } else { containerDetails.env![envVarIndex] = { ...originalKeyVal, value }; @@ -210,7 +205,7 @@ const AppConfigOverview = ({ containerDetails.env!.splice(envVarIndex, 1); // delete the env var out of the array checkForUnsavedChanges(containerName, newState); setEnvVariables(newState); - } + }; const onAddEnvVar = (containerName: string) => { const newState = getEnvVarsPartialDeepClone(containerName); @@ -249,7 +244,9 @@ const AppConfigOverview = ({
- ENVIRONMENT VARIABLES: "{containerDetails.containerName}" + + ENVIRONMENT VARIABLES: "{containerDetails.containerName}" + + @@ -380,10 +430,7 @@ const AppPromoCard = ({ iamRoleArnDefault={suggestedIamRoleArn} /> - theme.zIndex.drawer + 1 }} - open={spinning} - > + theme.zIndex.drawer + 1 }} open={spinning}> @@ -399,11 +446,11 @@ export const AppPromoWidget = () => { if (awsAppLoadingStatus.loading) { return ; } else if (awsAppLoadingStatus.component) { - const component = awsAppLoadingStatus.component + const component = awsAppLoadingStatus.component; const input = { awsComponent: component, catalogApi, - appEntity: entity + appEntity: entity, }; return ; diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppPromoCard/AwsEksEnvPromoDialog.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppPromoCard/AwsEksEnvPromoDialog.tsx index 23c4c041..3815e125 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppPromoCard/AwsEksEnvPromoDialog.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppPromoCard/AwsEksEnvPromoDialog.tsx @@ -6,8 +6,14 @@ import { Dialog, DialogActions, DialogContent, - DialogTitle, Grid, - IconButton, InputLabel, MenuItem, Select, TextField, makeStyles + DialogTitle, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + TextField, + makeStyles, } from '@material-ui/core'; import { Close } from '@mui/icons-material'; import React, { useState } from 'react'; @@ -60,18 +66,21 @@ export const AwsEksEnvPromoDialog = ({ namespaceDefault: string; iamRoleArnDefault: string; }) => { - const classes = useStyles(); - const [namespace, setNamespace] = useState(""); + const [namespace, setNamespace] = useState(''); const [namespaceIsInvalid, setNamespaceIsInvalid] = useState(false); - const [namespaceDescription, setNamespaceDescription] = useState(`The k8s namespace to assign to application resources for the ${environmentName} environment`); + const [namespaceDescription, setNamespaceDescription] = useState( + `The k8s namespace to assign to application resources for the ${environmentName} environment`, + ); - const [iamRoleArn, setIamRoleArn] = useState(""); + const [iamRoleArn, setIamRoleArn] = useState(''); const [iamRoleArnIsInvalid, setIamRoleArnIsInvalid] = useState(false); - const [iamRoleArnDescription, setIamRoleArnDescription] = useState("Existing IAM role to grant namespace privileges to"); + const [iamRoleArnDescription, setIamRoleArnDescription] = useState( + 'Existing IAM role to grant namespace privileges to', + ); - const [roleBehavior, setRoleBehavior] = useState("create_new_k8s_namespace_admin_iam_role"); + const [roleBehavior, setRoleBehavior] = useState('create_new_k8s_namespace_admin_iam_role'); const submitNewEnvironmentHandler = () => { if (roleBehavior === 'existing_new_k8s_namespace_admin_iam_role' && !iamRoleArn) { @@ -87,26 +96,35 @@ export const AwsEksEnvPromoDialog = ({ const checkNamespace = () => { if (!namespace) { - setNamespaceDescription("Cannot be Empty"); + setNamespaceDescription('Cannot be Empty'); setNamespaceIsInvalid(true); } else { - setNamespaceDescription(`The k8s namespace to assign to application resources for the ${environmentName} environment`); + setNamespaceDescription( + `The k8s namespace to assign to application resources for the ${environmentName} environment`, + ); setNamespaceIsInvalid(false); } }; const checkIamRoleArn = () => { if (!iamRoleArn) { - setIamRoleArnDescription("Cannot be Empty"); + setIamRoleArnDescription('Cannot be Empty'); setIamRoleArnIsInvalid(true); } else { - setIamRoleArnDescription("Existing IAM role to grant namespace privileges to"); + setIamRoleArnDescription('Existing IAM role to grant namespace privileges to'); setIamRoleArnIsInvalid(false); } }; return ( - + Add Environment: {environmentName} @@ -140,17 +158,23 @@ export const AwsEksEnvPromoDialog = ({ id="select-role-behavior" value={roleBehavior} label="Role Behavior" - onChange={(e: React.ChangeEvent<{ - name?: string | undefined; - value: unknown; - }>) => setRoleBehavior(e.target.value as string)} + onChange={( + e: React.ChangeEvent<{ + name?: string | undefined; + value: unknown; + }>, + ) => setRoleBehavior(e.target.value as string)} > - Create a separate role for the K8s namespace - Import existing role and grant it access to the K8s namespace + + Create a separate role for the K8s namespace + + + Import existing role and grant it access to the K8s namespace + - {roleBehavior === 'existing_new_k8s_namespace_admin_iam_role' && + {roleBehavior === 'existing_new_k8s_namespace_admin_iam_role' && ( IAM Role @@ -168,7 +192,7 @@ export const AwsEksEnvPromoDialog = ({ > - } + )} ); }; - diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppStateCard/AppStateCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppStateCard/AppStateCard.tsx index 313f1d35..87e55618 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppStateCard/AppStateCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppStateCard/AppStateCard.tsx @@ -13,8 +13,10 @@ import { useAsyncAwsApp } from '../../hooks/useAwsApp'; import { AWSComponent, AWSECSAppDeploymentEnvironment } from '@aws/plugin-aws-apps-common-for-backstage'; const OpaAppStateOverview = ({ - input: { cluster, serviceArn, taskDefArn } -}: { input: { cluster: string, serviceArn: string, taskDefArn: string, awsComponent: AWSComponent } }) => { + input: { cluster, serviceArn, taskDefArn }, +}: { + input: { cluster: string; serviceArn: string; taskDefArn: string; awsComponent: AWSComponent }; +}) => { const api = useApi(opaApiRef); const [taskData, setTaskData] = useState({}); @@ -30,7 +32,7 @@ const OpaAppStateOverview = ({ await sleep(5000); return api.getTaskDetails({ cluster: cluster, - service: serviceArn + service: serviceArn, }); } @@ -39,7 +41,6 @@ const OpaAppStateOverview = ({ entity and also task Data */ async function getData() { - const tasks = await api.getTaskDetails({ cluster, service: serviceArn, @@ -174,18 +175,18 @@ export const AppStateCard = () => { const awsAppLoadingStatus = useAsyncAwsApp(); if (awsAppLoadingStatus.loading) { - return + return ; } else if (awsAppLoadingStatus.component) { const env = awsAppLoadingStatus.component.currentEnvironment as AWSECSAppDeploymentEnvironment; - const latestTaskDef = env.app.taskDefArn.substring(0, env.app.taskDefArn.lastIndexOf(":")) + const latestTaskDef = env.app.taskDefArn.substring(0, env.app.taskDefArn.lastIndexOf(':')); const input = { cluster: env.clusterName, serviceArn: env.app.serviceArn, taskDefArn: latestTaskDef, - awsComponent: awsAppLoadingStatus.component + awsComponent: awsAppLoadingStatus.component, }; - return + return ; } else { - return + return ; } }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/AppStateCardCloudFormation/AppStateCardCloudFormation.tsx b/backstage-plugins/plugins/aws-apps/src/components/AppStateCardCloudFormation/AppStateCardCloudFormation.tsx index 307001e6..38e4073e 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AppStateCardCloudFormation/AppStateCardCloudFormation.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AppStateCardCloudFormation/AppStateCardCloudFormation.tsx @@ -6,25 +6,37 @@ import { useApi } from '@backstage/core-plugin-api'; import { LinearProgress } from '@material-ui/core'; import { Button, CardContent, Divider, Grid, Typography } from '@mui/material'; import React, { useState, useEffect, useRef } from 'react'; -import { DescribeStackEventsCommandOutput, UpdateStackCommandOutput, CreateStackCommandOutput, DeleteStackCommandOutput, } from "@aws-sdk/client-cloudformation"; +import { + DescribeStackEventsCommandOutput, + UpdateStackCommandOutput, + CreateStackCommandOutput, + DeleteStackCommandOutput, +} from '@aws-sdk/client-cloudformation'; import { opaApiRef } from '../../api'; import { useAsyncAwsApp } from '../../hooks/useAwsApp'; import { formatWithTime } from '../../helpers/date-utils'; import { useCancellablePromise } from '../../hooks/useCancellablePromise'; -import { AWSComponent, AWSServerlessAppDeploymentEnvironment, CloudFormationStack, getGitCredentailsSecret } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSComponent, + AWSServerlessAppDeploymentEnvironment, + CloudFormationStack, + getGitCredentailsSecret, +} from '@aws/plugin-aws-apps-common-for-backstage'; type stackEvent = { action: string | undefined; resourceType: string | undefined; logicalResourceId: string | undefined; -} +}; -const eventStyle = { paddingRight: "15px" }; +const eventStyle = { paddingRight: '15px' }; const OpaAppStateOverview = ({ - input: { awsComponent, stack, s3BucketName, refresh } -}: { input: { awsComponent: AWSComponent, stack: CloudFormationStack, s3BucketName: string, refresh: VoidFunction } }) => { - const api = useApi(opaApiRef) + input: { awsComponent, stack, s3BucketName, refresh }, +}: { + input: { awsComponent: AWSComponent; stack: CloudFormationStack; s3BucketName: string; refresh: VoidFunction }; +}) => { + const api = useApi(opaApiRef); const [polling, setPolling] = useState(false); const [pollingEnabled, setPollingEnabled] = useState(true); @@ -35,9 +47,8 @@ const OpaAppStateOverview = ({ const repoInfo = awsComponent.getRepoInfo(); useEffect(() => { - if (pollingEnabled && stack.stackDeployStatus.endsWith('PROGRESS')) { - getStackEvents().then(_ => { }); + getStackEvents().then(_ => {}); } return () => { @@ -45,9 +56,7 @@ const OpaAppStateOverview = ({ if (timerRef.current) { clearTimeout(timerRef.current); } - - } - + }; }, []); /* @@ -64,18 +73,21 @@ const OpaAppStateOverview = ({ } count++; try { - const stackEvents = await cancellablePromise(api.getStackEvents({ - stackName: stack.stackName, - })); + const stackEvents = await cancellablePromise( + api.getStackEvents({ + stackName: stack.stackName, + }), + ); if (!stackEvents.StackEvents) { break; } const mostRecentEvent = stackEvents.StackEvents[0]; - const isDone = mostRecentEvent.ResourceType === 'AWS::CloudFormation::Stack' - && !!mostRecentEvent.ResourceStatus - && (mostRecentEvent.ResourceStatus!.endsWith('COMPLETE') || mostRecentEvent.ResourceStatus!.endsWith('FAILED')) + const isDone = + mostRecentEvent.ResourceType === 'AWS::CloudFormation::Stack' && + !!mostRecentEvent.ResourceStatus && + (mostRecentEvent.ResourceStatus!.endsWith('COMPLETE') || mostRecentEvent.ResourceStatus!.endsWith('FAILED')); if (isDone) { break; @@ -89,7 +101,7 @@ const OpaAppStateOverview = ({ } let maxEventsShown = 5; - if (stackEvents.StackEvents.length < (maxEventsShown)) { + if (stackEvents.StackEvents.length < maxEventsShown) { maxEventsShown = stackEvents.StackEvents.length; } @@ -98,12 +110,11 @@ const OpaAppStateOverview = ({ return { action: ev.ResourceStatus, resourceType: ev.ResourceType, - logicalResourceId: ev.LogicalResourceId + logicalResourceId: ev.LogicalResourceId, }; }); setEvents(visibleEvents); - } catch (e) { if ((e as any).isCanceled) { isCanceled = true; @@ -113,23 +124,20 @@ const OpaAppStateOverview = ({ } break; } - } if (!isCanceled) { setPolling(false); refresh(); } - } function sleep(ms: number) { return new Promise(resolve => { - const resolveHandler = () => { clearTimeout(timerRef.current); resolve(null); - } + }; timerRef.current = setTimeout(resolveHandler, ms); }); } @@ -140,23 +148,24 @@ const OpaAppStateOverview = ({ } else { return handleUpdateDeployment(); } - } + }; const handleUpdateDeployment = async () => { - setEvents([]); setPolling(true); try { - await cancellablePromise(api.updateStack({ - componentName: awsComponent.componentName, - stackName: stack.stackName, - s3BucketName, - cfFileName: "packaged.yaml", - environmentName: awsComponent.currentEnvironment.environment.name, - repoInfo, - gitAdminSecret: getGitCredentailsSecret(repoInfo), - })); + await cancellablePromise( + api.updateStack({ + componentName: awsComponent.componentName, + stackName: stack.stackName, + s3BucketName, + cfFileName: 'packaged.yaml', + environmentName: awsComponent.currentEnvironment.environment.name, + repoInfo, + gitAdminSecret: getGitCredentailsSecret(repoInfo), + }), + ); getStackEvents(); } catch (e) { @@ -165,24 +174,24 @@ const OpaAppStateOverview = ({ setError({ isError: true, errorMsg: `Unexpected error occurred while updating the deployment: ${e}` }); } } - }; const handleCreateDeployment = async () => { - setEvents([]); setPolling(true); try { - await cancellablePromise(api.createStack({ - componentName: awsComponent.componentName, - stackName: stack.stackName, - s3BucketName, - cfFileName: "packaged.yaml", - environmentName: awsComponent.currentEnvironment.environment.name, - repoInfo, - gitAdminSecret: getGitCredentailsSecret(repoInfo), - })); + await cancellablePromise( + api.createStack({ + componentName: awsComponent.componentName, + stackName: stack.stackName, + s3BucketName, + cfFileName: 'packaged.yaml', + environmentName: awsComponent.currentEnvironment.environment.name, + repoInfo, + gitAdminSecret: getGitCredentailsSecret(repoInfo), + }), + ); getStackEvents(); } catch (e) { @@ -191,7 +200,6 @@ const OpaAppStateOverview = ({ setError({ isError: true, errorMsg: `Unexpected error occurred while creating the deployment: ${e}` }); } } - }; const handleStopApp = async () => { @@ -199,10 +207,12 @@ const OpaAppStateOverview = ({ setPolling(true); try { - await cancellablePromise(api.deleteStack({ - componentName: awsComponent.componentName, - stackName: stack.stackName, - })); + await cancellablePromise( + api.deleteStack({ + componentName: awsComponent.componentName, + stackName: stack.stackName, + }), + ); getStackEvents(); } catch (e) { @@ -224,10 +234,10 @@ const OpaAppStateOverview = ({ const getStatus = (stackStatus: string): string => { if (stackStatus === 'CREATE_COMPLETE' || stackStatus === 'UPDATE_COMPLETE') { - return "LIVE"; + return 'LIVE'; } return stackStatus; - } + }; if (error.isError) { return {error.errorMsg}; @@ -236,43 +246,46 @@ const OpaAppStateOverview = ({ return ( - {polling && + {polling && ( <> - {events.length > 0 && + {events.length > 0 && ( <> Latest events as of {formatWithTime(new Date())}... -

+
+
- - - + + + - {events.map((stackEvent, i) => - - - - )} + {events.map((stackEvent, i) => ( + + + + + + ))}
ActionResource TypeResource ID + Action + + Resource Type + + Resource ID +
{stackEvent.action}{stackEvent.resourceType}{stackEvent.logicalResourceId}
{stackEvent.action}{stackEvent.resourceType}{stackEvent.logicalResourceId}

- } + )} <>Polling for updates...
- - } - {!polling && + )} + {!polling && ( @@ -282,26 +295,16 @@ const OpaAppStateOverview = ({ Created At - - {stack.creationTime || ''} - + {stack.creationTime || ''} Last Updated - - {stack.lastUpdatedTime || ''} - + {stack.lastUpdatedTime || ''} - - + + - theme.zIndex.drawer + 1 }} - open={spinning} - > + theme.zIndex.drawer + 1 }} open={spinning}>
@@ -267,7 +299,7 @@ export const AwsEnvironmentProviderCardWidget = () => { const input = { entity, - catalog: catalogApi + catalog: catalogApi, }; return ; }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/AwsEnvironmentProviderCard/AwsEnvironmentProviderSelectorDialog.tsx b/backstage-plugins/plugins/aws-apps/src/components/AwsEnvironmentProviderCard/AwsEnvironmentProviderSelectorDialog.tsx index b752fc6c..df9e1523 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/AwsEnvironmentProviderCard/AwsEnvironmentProviderSelectorDialog.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/AwsEnvironmentProviderCard/AwsEnvironmentProviderSelectorDialog.tsx @@ -7,8 +7,12 @@ import { Dialog, DialogActions, DialogContent, - DialogTitle, Grid, - IconButton, InputLabel, MenuItem, makeStyles + DialogTitle, + Grid, + IconButton, + InputLabel, + MenuItem, + makeStyles, } from '@material-ui/core'; import { Close } from '@mui/icons-material'; import React, { useEffect, useState } from 'react'; @@ -47,21 +51,20 @@ export const AwsEnvironmentProviderSelectorDialog = ({ isOpen, closeDialogHandler, selectHandler, - providersInput + providersInput, }: { isOpen: boolean; closeDialogHandler: () => void; selectHandler: (item: AWSEnvironmentProviderRecord) => void; providersInput: AWSEnvironmentProviderRecord[]; }) => { - const classes = useStyles(); const [selectedProvider, setSelectedProvider] = useState(); const handleChangeSelectedProvider = (event: SelectChangeEvent) => { const selectedProvider = event.target.value as string; const matchingProviders = providersInput.filter(providerRecord => { - return selectedProvider === `${providerRecord.prefix}:${providerRecord.name}` + return selectedProvider === `${providerRecord.prefix}:${providerRecord.name}`; }); if (matchingProviders.length != 1) { @@ -77,30 +80,31 @@ export const AwsEnvironmentProviderSelectorDialog = ({ selectHandler(selectedProvider); } closeDialogHandler(); - } + }; if (!selectedProvider && providersInput.length > 0) { - setSelectedProvider(providersInput[0]) + setSelectedProvider(providersInput[0]); } const selectorProviders = providersInput.map(p => { const key = `${p.prefix}:${p.name}`; const title = `${p.prefix}:${p.name}`; - return ({title}) + return ( + + {title} + + ); }); const getSelectedProvider = () => { if (selectedProvider) { return `${selectedProvider?.prefix}:${selectedProvider?.name}`; + } else { + return; } - else { - return - } - } - - useEffect(() => { + }; - }, []); + useEffect(() => {}, []); return ( @@ -114,7 +118,8 @@ export const AwsEnvironmentProviderSelectorDialog = ({ Providers - { }; return ; } else { - return ; + return ( + + ); } }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/GeneralInfoCard/GeneralInfoCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/GeneralInfoCard/GeneralInfoCard.tsx index c4868beb..d21ed9e4 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/GeneralInfoCard/GeneralInfoCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/GeneralInfoCard/GeneralInfoCard.tsx @@ -15,33 +15,31 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; const OpaAppGeneralInfo = ({ - input: { entity, repoSecretArn, api } -}: { input: { account: string, region: string, entity: Entity, repoSecretArn: string, api: OPAApi} }) => { - + input: { entity, repoSecretArn, api }, +}: { + input: { account: string; region: string; entity: Entity; repoSecretArn: string; api: OPAApi }; +}) => { const [loading, setLoading] = useState(true); const [error, setError] = useState<{ isError: boolean; errorMsg: string | null }>({ isError: false, errorMsg: null }); - - const [secretData, setSecretData] = useState(""); - + + const [secretData, setSecretData] = useState(''); + let repoInfo = getRepoInfo(entity); const gitRepoUrl = getRepoUrl(repoInfo); -// const getGitAppUrl = () => { -// const gitAppUrl = gitHost + "/" + gitApp + ".git" -// return gitAppUrl -// } + // const getGitAppUrl = () => { + // const gitAppUrl = gitHost + "/" + gitApp + ".git" + // return gitAppUrl + // } const HandleCopyGitClone = () => { - let baseUrl = "git clone https://oauth2:" - let cloneUrl = "" - if (!repoSecretArn) - { - baseUrl = "git clone https://" + let baseUrl = 'git clone https://oauth2:'; + let cloneUrl = ''; + if (!repoSecretArn) { + baseUrl = 'git clone https://'; cloneUrl = baseUrl + gitRepoUrl; - } - else - { - cloneUrl = baseUrl + secretData + "@" + gitRepoUrl; + } else { + cloneUrl = baseUrl + secretData + '@' + gitRepoUrl; } navigator.clipboard.writeText(cloneUrl); }; @@ -52,21 +50,17 @@ const OpaAppGeneralInfo = ({ async function getData() { if (!repoSecretArn) { - setSecretData(""); - } - else - { + setSecretData(''); + } else { const secrets = await api.getPlatformSecret({ secretName: repoSecretArn, }); - console.log(secrets) - setSecretData(secrets.SecretString || ""); + console.log(secrets); + setSecretData(secrets.SecretString || ''); } - } useEffect(() => { - getData() .then(() => { setLoading(false); @@ -95,11 +89,11 @@ const OpaAppGeneralInfo = ({ - { - repoSecretArn? - ( - <> - Repository Access Token + {repoSecretArn ? ( + <> + + Repository Access Token + @@ -107,10 +101,9 @@ const OpaAppGeneralInfo = ({ - ): - (<>) - } - + ) : ( + <> + )} Clone url @@ -118,11 +111,13 @@ const OpaAppGeneralInfo = ({ - + @@ -149,12 +144,10 @@ export const GeneralInfoCard = ({ appPending }: { appPending: boolean }) => { api, }; return ; - } - else { + } else { if (awsAppLoadingStatus.loading) { return ; } else if (awsAppLoadingStatus.component) { - const env = awsAppLoadingStatus.component.currentEnvironment as AWSECSAppDeploymentEnvironment; const input = { @@ -162,12 +155,11 @@ export const GeneralInfoCard = ({ appPending }: { appPending: boolean }) => { region: env.providerData.region, entity: entity, repoSecretArn: awsAppLoadingStatus.component.repoSecretArn, - api + api, }; return ; } else { return ; } } - }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/GenericTable/GenericTable.tsx b/backstage-plugins/plugins/aws-apps/src/components/GenericTable/GenericTable.tsx index 86fa2e0f..3ba074b6 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/GenericTable/GenericTable.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/GenericTable/GenericTable.tsx @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - import React, { useEffect, useState } from 'react'; import { TableColumn, Table } from '@backstage/core-components'; import { makeStyles } from '@material-ui/core/styles'; diff --git a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/InfrastructureCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/InfrastructureCard.tsx index 9e1e2b88..6bf0e762 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/InfrastructureCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/InfrastructureCard.tsx @@ -3,7 +3,13 @@ import { InfoCard, EmptyState } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { AWSComponent, AWSComponentType, AWSECSAppDeploymentEnvironment, AWSServiceResources, AWSResourceDeploymentEnvironment } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSComponent, + AWSComponentType, + AWSECSAppDeploymentEnvironment, + AWSServiceResources, + AWSResourceDeploymentEnvironment, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { LinearProgress, Typography } from '@material-ui/core'; import React, { useEffect, useState } from 'react'; import { opaApiRef } from '../../api'; @@ -12,16 +18,16 @@ import { useAsyncAwsApp } from '../../hooks/useAwsApp'; import { ProviderType } from '../../helpers/constants'; const OpaAppInfraInfo = ({ - input: { resourceGroupArn, awsComponent } -}: { input: { resourceGroupArn: string, awsComponent: AWSComponent } }) => { - + input: { resourceGroupArn, awsComponent }, +}: { + input: { resourceGroupArn: string; awsComponent: AWSComponent }; +}) => { const api = useApi(opaApiRef); const [rscGroupData, setRscGroupData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState<{ isError: boolean; errorMsg: string | null }>({ isError: false, errorMsg: null }); - // The default set of AWS services to show on the Infrastructure card // TODO: this should be externalized to configuration const defaultServiceFilter = [ @@ -50,7 +56,7 @@ const OpaAppInfraInfo = ({ } const rscGroupResources = await api.getResourceGroupResources({ - rscGroupArn: resourceGroupArn + rscGroupArn: resourceGroupArn, }); const data = rscGroupResources ?? {}; @@ -65,8 +71,10 @@ const OpaAppInfraInfo = ({ }) .catch(e => { const statusCode = e.body.response.statusCode ?? null; - const errorMsg = statusCode === 404 ? 'Data not available at this time' - : `Unexpected error occurred while retrieving resource group data: ${e}`; + const errorMsg = + statusCode === 404 + ? 'Data not available at this time' + : `Unexpected error occurred while retrieving resource group data: ${e}`; setError({ isError: true, errorMsg }); setLoading(false); }); @@ -88,14 +96,18 @@ const OpaAppInfraInfo = ({ return ( - + ); }; @@ -104,28 +116,32 @@ export const InfrastructureCard = () => { const awsAppLoadingStatus = useAsyncAwsApp(); if (awsAppLoadingStatus.loading) { - return + return ; } else if (awsAppLoadingStatus.component) { let input = undefined; if (awsAppLoadingStatus.component.componentType === AWSComponentType.AWSApp) { const env = awsAppLoadingStatus.component.currentEnvironment as AWSECSAppDeploymentEnvironment; input = { resourceGroupArn: env.app.resourceGroupArn, - awsComponent: awsAppLoadingStatus.component + awsComponent: awsAppLoadingStatus.component, }; - } - else if (awsAppLoadingStatus.component.componentType === AWSComponentType.AWSResource) { + } else if (awsAppLoadingStatus.component.componentType === AWSComponentType.AWSResource) { const env = awsAppLoadingStatus.component.currentEnvironment as AWSResourceDeploymentEnvironment; input = { resourceGroupArn: env.resource.resourceGroupArn, - awsComponent: awsAppLoadingStatus.component + awsComponent: awsAppLoadingStatus.component, }; + } else { + throw new Error('Infrastructure Card Not yet implemented!'); } - else { - throw new Error("Infrastructure Card Not yet implemented!") - } - return + return ; } else { - return + return ( + + ); } }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ResourceDetailsDialog.tsx b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ResourceDetailsDialog.tsx index df5f6b0a..4f6d9ebb 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ResourceDetailsDialog.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ResourceDetailsDialog.tsx @@ -9,8 +9,12 @@ import { Dialog, DialogActions, DialogContent, - DialogTitle, Grid, - IconButton, LinearProgress, makeStyles, Typography + DialogTitle, + Grid, + IconButton, + LinearProgress, + makeStyles, + Typography, } from '@material-ui/core'; import { Close } from '@mui/icons-material'; import React, { useEffect, useState } from 'react'; @@ -39,7 +43,6 @@ const useStyles = makeStyles(theme => ({ }, })); - /** * A Table component for showing AWS Resource details. The table can accommodate varying numbers of columns * with corresponding table data @@ -82,9 +85,9 @@ export const ResourceDetailsDialog = ({ isOpen, closeDialogHandler, resource, - // prefix, - // providerName -}: { +}: // prefix, +// providerName +{ isOpen: boolean; closeDialogHandler: () => void; resource: AWSResource; @@ -126,7 +129,6 @@ export const ResourceDetailsDialog = ({ // Get the secret details async function getData() { - if (resource.resourceTypeId == 'AWS::SecretsManager::Secret') { const secretResponse = await api.getSecret({ secretName: resource.resourceArn }); const rawSecret = secretResponse.SecretString ?? 'unknown'; @@ -136,8 +138,12 @@ export const ResourceDetailsDialog = ({ const parsedSecret = JSON.parse(rawSecret); setTableColumns(kvColumns); const jsonKeys = Object.keys(parsedSecret).sort((a, b) => { - if (a < b) { return -1; } - if (a > b) { return 1; } + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } return 0; }); const secretTableData = jsonKeys.map((key, i) => { @@ -156,7 +162,7 @@ export const ResourceDetailsDialog = ({ } } else if (resource.resourceTypeId == 'AWS::SSM::Parameter') { const ssmParamResponse = await api.getSSMParameter({ - ssmParamName: resource.resourceName + ssmParamName: resource.resourceName, }); // SSM Parameters are single-value and will only be displayed in a single column table setTableColumns(singleColumn); @@ -208,4 +214,3 @@ export const ResourceDetailsDialog = ({ ); }; - diff --git a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ServiceComponent.tsx b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ServiceComponent.tsx index aeac09fc..2fa16dc7 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ServiceComponent.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/InfrastructureCard/ServiceComponent.tsx @@ -38,7 +38,15 @@ const useStyles = makeStyles(theme => ({ * @param resource The AWS resource type requiring a link to display its details. * @returns JSXElement */ -const CustomDetailsLink = ({ resource, prefix, providerName }: { resource: AWSResource, prefix: string, providerName: string }) => { +const CustomDetailsLink = ({ + resource, + prefix, + providerName, +}: { + resource: AWSResource; + prefix: string; + providerName: string; +}) => { const [open, setOpen] = useState(false); const openDialog = () => { @@ -51,7 +59,13 @@ const CustomDetailsLink = ({ resource, prefix, providerName }: { resource: AWSRe details - + ); }; @@ -63,10 +77,23 @@ const CustomDetailsLink = ({ resource, prefix, providerName }: { resource: AWSRe * @param resources An array of AWS resource objects which below to the specified serviceName * @returns JSXElement rendering a table for an AWS service and its resources */ -export const DenseResourceTable = ({ serviceName, resources, prefix, providerName }: { serviceName?: string; resources: AWSResource[]; prefix: string; providerName: string; }) => { +export const DenseResourceTable = ({ + serviceName, + resources, + prefix, + providerName, +}: { + serviceName?: string; + resources: AWSResource[]; + prefix: string; + providerName: string; +}) => { const classes = useStyles(); - const preventRerender = useCallback((row: any): React.ReactNode => , []); + const preventRerender = useCallback( + (row: any): React.ReactNode => , + [], + ); // Table column definition used for displaying key/value pairs of resource type and resource name const columns: TableColumn[] = [ @@ -92,7 +119,9 @@ export const DenseResourceTable = ({ serviceName, resources, prefix, providerNam const detailTypes = ['AWS::SecretsManager::Secret', 'AWS::SSM::Parameter']; // subvalue is a details link to be shown beneath a table cell value - const subvalue = detailTypes.includes(r.resourceTypeId) ? : undefined; + const subvalue = detailTypes.includes(r.resourceTypeId) ? ( + + ) : undefined; return { id: i, @@ -129,7 +158,17 @@ export const DenseResourceTable = ({ serviceName, resources, prefix, providerNam * @param serviceName A short string describing the AWS service. * @param resources An array of AWS resource objects which below to the specified serviceName */ -const Service = ({ serviceName, resources, prefix, providerName }: { serviceName: string; resources: AWSResource[], prefix: string, providerName: string }) => { +const Service = ({ + serviceName, + resources, + prefix, + providerName, +}: { + serviceName: string; + resources: AWSResource[]; + prefix: string; + providerName: string; +}) => { const classes = useStyles(); if (!resources || resources.length === 0) { @@ -155,7 +194,7 @@ export const ServiceResourcesComponent = ({ servicesObject, serviceFilter = [], prefix, - providerName + providerName, }: { servicesObject: AWSServiceResources; serviceFilter?: string[]; @@ -166,12 +205,24 @@ export const ServiceResourcesComponent = ({ const filteredKeys = serviceFilter.length == 0 ? svcKeys : serviceFilter.filter(value => svcKeys.includes(value)); filteredKeys.sort((a, b) => { - if (a < b) { return -1; } - if (a > b) { return 1; } + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } return 0; }); const serviceItems = filteredKeys.map((serviceName, i) => { - return !resource.resourceName.includes('AutoDelete'))} prefix={prefix} providerName={providerName} />; + return ( + !resource.resourceName.includes('AutoDelete'))} + prefix={prefix} + providerName={providerName} + /> + ); }); return <>{serviceItems}; diff --git a/backstage-plugins/plugins/aws-apps/src/components/K8sAppStateCard/K8sAppStateCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/K8sAppStateCard/K8sAppStateCard.tsx index db40e061..04ee040b 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/K8sAppStateCard/K8sAppStateCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/K8sAppStateCard/K8sAppStateCard.tsx @@ -3,7 +3,14 @@ import { InvokeCommandOutput } from '@aws-sdk/client-lambda'; import { GetParameterCommandOutput } from '@aws-sdk/client-ssm'; -import { AWSComponent, AWSEKSAppDeploymentEnvironment, AppState, AppStateType, KeyValue, getGitCredentailsSecret } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSComponent, + AWSEKSAppDeploymentEnvironment, + AppState, + AppStateType, + KeyValue, + getGitCredentailsSecret, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { Entity } from '@backstage/catalog-model'; import { EmptyState, InfoCard } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -65,8 +72,7 @@ const StyledInput = styled('input')( color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - box-shadow: 0px 2px 4px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.5)' : 'rgba(0,0,0, 0.05)' - }; + box-shadow: 0px 2px 4px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.5)' : 'rgba(0,0,0, 0.05)'}; border-radius: 8px; margin: 0 8px; padding: 10px 12px; @@ -129,40 +135,39 @@ const StyledButton = styled('button')( ); const OpaAppStateOverview = ({ - input: { env, entity, awsComponent } + input: { env, entity, awsComponent }, }: { input: { - env: AWSEKSAppDeploymentEnvironment, - entity: Entity, - awsComponent: AWSComponent - } + env: AWSEKSAppDeploymentEnvironment; + entity: Entity; + awsComponent: AWSComponent; + }; }) => { - const api = useApi(opaApiRef); const [appStateData, setAppStateData] = useState([]); const [variablesJson, setVariablesJson] = useState({}); const [appStarted, setAppStarted] = useState(false); const [appStopped, setAppStopped] = useState(false); - const [clusterNameState, setClusterNameState] = useState(""); + const [clusterNameState, setClusterNameState] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState<{ isError: boolean; errorMsg: string | null }>({ isError: false, errorMsg: null }); const { cancellablePromise } = useCancellablePromise({ rejectOnCancel: true }); const timerRef = useRef(null); const repoInfo = awsComponent.getRepoInfo(); - + // Namespace-bound application admin role (not cluster admin role) const appAdminRoleArn = env.app.appAdminRoleArn; - const kubectlLambdaArn = env.entities.envProviderEntity?.metadata["kubectlLambdaArn"]?.toString() || ""; + const kubectlLambdaArn = env.entities.envProviderEntity?.metadata['kubectlLambdaArn']?.toString() || ''; let clusterNameParam, clusterName: string; async function fetchAppConfig() { if (!clusterName) { // console.log(`getting cluster name`); clusterNameParam = await cancellablePromise( - api.getSSMParameter({ ssmParamName: env.clusterName }) + api.getSSMParameter({ ssmParamName: env.clusterName }), ); - clusterName = clusterNameParam.Parameter?.Value?.toString().split('/')[1].toString() || ""; + clusterName = clusterNameParam.Parameter?.Value?.toString().split('/')[1].toString() || ''; } else { // console.log(`clusterName was already cached when getting app config`); } @@ -170,18 +175,18 @@ const OpaAppStateOverview = ({ setClusterNameState(clusterName); const bodyParamVariables = { - RequestType: "Create", - ResourceType: "Custom::AWSCDK-EKS-KubernetesObjectValue", + RequestType: 'Create', + ResourceType: 'Custom::AWSCDK-EKS-KubernetesObjectValue', ResourceProperties: { - TimeoutSeconds: "5", + TimeoutSeconds: '5', ClusterName: clusterName, RoleArn: appAdminRoleArn, ObjectNamespace: env.app.namespace, InvocationType: 'RequestResponse', - ObjectType: "configmaps", + ObjectType: 'configmaps', ObjectLabels: `app.kubernetes.io/env=${env.environment.name},app.kubernetes.io/name=${entity.metadata.name}`, - JsonPath: "@" - } + JsonPath: '@', + }, }; // console.log(`calling lambda to get configs`); @@ -189,8 +194,8 @@ const OpaAppStateOverview = ({ api.invokeLambda({ functionName: kubectlLambdaArn, actionDescription: `Fetch app configs for namespace ${env.app.namespace}`, - body: JSON.stringify(bodyParamVariables) - }) + body: JSON.stringify(bodyParamVariables), + }), ); // console.log(`got configs`); @@ -206,41 +211,37 @@ const OpaAppStateOverview = ({ } else { return {}; } - } - } catch (err) { console.log(err); throw Error("Can't parse json response"); } - } async function fetchAppState() { - if (!clusterName) { // console.log(`getting cluster name`); clusterNameParam = await cancellablePromise( - api.getSSMParameter({ ssmParamName: env.clusterName }) + api.getSSMParameter({ ssmParamName: env.clusterName }), ); // console.log(`DONE getting cluster name`); - clusterName = clusterNameParam.Parameter?.Value?.toString().split('/')[1].toString() || ""; + clusterName = clusterNameParam.Parameter?.Value?.toString().split('/')[1].toString() || ''; // console.log(`clusterName is ${clusterName}`); } const bodyParam = { - RequestType: "Create", - ResourceType: "Custom::AWSCDK-EKS-KubernetesObjectValue", + RequestType: 'Create', + ResourceType: 'Custom::AWSCDK-EKS-KubernetesObjectValue', ResourceProperties: { - TimeoutSeconds: "5", + TimeoutSeconds: '5', ClusterName: clusterName, RoleArn: appAdminRoleArn, ObjectNamespace: env.app.namespace, InvocationType: 'RequestResponse', - ObjectType: "deployments", + ObjectType: 'deployments', ObjectLabels: `app.kubernetes.io/env=${env.environment.name},app.kubernetes.io/name=${entity.metadata.name}`, - JsonPath: "@" - } + JsonPath: '@', + }, }; // console.log(bodyParam) @@ -249,8 +250,8 @@ const OpaAppStateOverview = ({ api.invokeLambda({ functionName: kubectlLambdaArn, actionDescription: `Fetch deployments for namespace ${env.app.namespace}`, - body: JSON.stringify(bodyParam) - }) + body: JSON.stringify(bodyParam), + }), ); // console.log(`got manifests`); @@ -266,7 +267,6 @@ const OpaAppStateOverview = ({ return {}; } } - } catch (err) { console.log(err); throw Error("Can't parse json response"); @@ -278,13 +278,16 @@ const OpaAppStateOverview = ({ return []; } - const configMapName = appStateData.filter(appState => appState.appID === deploymentName)[0].stateObject.spec.template.spec.containers[0]?.envFrom?.[0]?.configMapRef?.name; + const configMapName = appStateData.filter(appState => appState.appID === deploymentName)[0].stateObject.spec + .template.spec.containers[0]?.envFrom?.[0]?.configMapRef?.name; if (!configMapName) { return []; } - const configMap = variablesJson.items.filter((candidateMap: any) => candidateMap.metadata.name === configMapName)?.[0]; + const configMap = variablesJson.items.filter( + (candidateMap: any) => candidateMap.metadata.name === configMapName, + )?.[0]; if (!configMap) { return []; @@ -295,8 +298,8 @@ const OpaAppStateOverview = ({ variables.push({ id: `${index}`, key: key.toString(), - value: configMap.data[key].toString() - }) + value: configMap.data[key].toString(), + }); }); return variables; }; @@ -304,9 +307,8 @@ const OpaAppStateOverview = ({ const parseState = (deploymentsJson: any): AppState[] => { // parse response JSON - let deploymentsState: AppState[] = [] + let deploymentsState: AppState[] = []; try { - Object.keys(deploymentsJson).forEach(key => { const deploymentJson = deploymentsJson[key]; @@ -330,20 +332,18 @@ const OpaAppStateOverview = ({ pendingCount: pending, runningCount: appRunning, lastStateTimestamp: new Date(deploymentJson.status.conditions[0].lastUpdateTime), - stateObject: deploymentJson - } + stateObject: deploymentJson, + }; deploymentsState.push(appState); - }) - + }); } catch (err) { console.log(err); } return deploymentsState || []; - } + }; async function getData(appStateResults?: any) { - let isCanceled = false; let isError = false; let deploymentsJson; @@ -353,8 +353,8 @@ const OpaAppStateOverview = ({ // console.log(`reusing appStateResults`); } - deploymentsJson = appStateResults ? appStateResults : await fetchAppState(); // returns array of deployments - variablesJson = await fetchAppConfig(); // return the configMaps for the app + deploymentsJson = appStateResults ? appStateResults : await fetchAppState(); // returns array of deployments + variablesJson = await fetchAppConfig(); // return the configMaps for the app } catch (e) { if ((e as any).isCanceled) { isCanceled = true; @@ -364,7 +364,6 @@ const OpaAppStateOverview = ({ console.error(e); setError({ isError: true, errorMsg: `Unexpected error occurred while retrieving event data: ${e}` }); } - } if (!isCanceled && !isError) { @@ -373,11 +372,10 @@ const OpaAppStateOverview = ({ setAppStateData(states); setVariablesJson(variablesJson); } - } useEffect(() => { - setAppStateData([]); // reset existing state + setAppStateData([]); // reset existing state getData() .then(() => { setLoading(false); @@ -397,23 +395,20 @@ const OpaAppStateOverview = ({ clearTimeout(timerRef.current); // console.log(`Clearing Timeout`); } - - } + }; }, []); function sleep(ms: number) { return new Promise(resolve => { - const resolveHandler = () => { clearTimeout(timerRef.current); resolve(null); - } + }; timerRef.current = setTimeout(resolveHandler, ms); }); } const handleStartTask = async (appState: AppState) => { - setLoading(true); // console.log(`calling lambda to set replicas to > 0, clusterNameState is ${clusterNameState}`); @@ -425,13 +420,13 @@ const OpaAppStateOverview = ({ actionDescription: `Starting app in environment ${env.environment.name}`, envName: env.environment.name, cluster: clusterNameState, - kubectlLambda: env.entities.envProviderEntity?.metadata["kubectlLambdaArn"]?.toString() || "", + kubectlLambda: env.entities.envProviderEntity?.metadata['kubectlLambdaArn']?.toString() || '', lambdaRoleArn: appAdminRoleArn, gitAdminSecret: getGitCredentailsSecret(repoInfo), updateKey: 'spec.replicas', updateValue: appState.desiredCount || 1, repoInfo, - }) + }), ); // console.log(`DONE setting replicas to > 0`); } catch (e) { @@ -449,7 +444,6 @@ const OpaAppStateOverview = ({ let localAppStarted = false; // console.log(`isCanceled is ${isCanceled} and localAppStarted is ${localAppStarted} and appStarted is ${appStarted}`); while (!isCanceled && !appStarted && !localAppStarted) { - // console.log(`sleeping waiting for app to be started, localAppStarted is ${localAppStarted} and appStarted is ${appStarted}`); await sleep(5000); // console.log(`awake ${count}, will now check app state`); @@ -463,7 +457,7 @@ const OpaAppStateOverview = ({ const currState = deploymentsJson[key]; if (currState.metadata.uid === appState.deploymentIdentifier) { if (Number.parseInt(currState.status.readyReplicas) > 0) { - setAppStateData([]); // reset existing state + setAppStateData([]); // reset existing state setAppStarted(true); localAppStarted = true; // console.log(`setting appStarted to true`); @@ -477,7 +471,6 @@ const OpaAppStateOverview = ({ } else { // console.log(`not breaking from while loop since appStarted is falsy`); } - } catch (e) { if ((e as any).isCanceled) { isCanceled = true; @@ -511,13 +504,13 @@ const OpaAppStateOverview = ({ actionDescription: `Stopping app in environment ${env.environment.name}`, envName: env.environment.name, cluster: clusterNameState, - kubectlLambda: env.entities.envProviderEntity?.metadata["kubectlLambdaArn"]?.toString() || "", + kubectlLambda: env.entities.envProviderEntity?.metadata['kubectlLambdaArn']?.toString() || '', lambdaRoleArn: appAdminRoleArn, gitAdminSecret: getGitCredentailsSecret(repoInfo), updateKey: 'spec.replicas', updateValue: 0, repoInfo, - }) + }), ); // console.log(`DONE setting replicas to 0`); } catch (e) { @@ -548,8 +541,8 @@ const OpaAppStateOverview = ({ const currState = deploymentsJson[key]; if (currState.metadata.uid === appState.deploymentIdentifier) { if (!currState.status.readyReplicas || Number.parseInt(currState.status.readyReplicas) === 0) { - appState.appState = AppStateType.STOPPED - appState.runningCount = 0 + appState.appState = AppStateType.STOPPED; + appState.runningCount = 0; localAppStopped = true; setAppStopped(true); setLoading(false); @@ -562,7 +555,6 @@ const OpaAppStateOverview = ({ // console.log(`breaking from while loop since app was stopped`); break; } - } catch (e) { if ((e as any).isCanceled) { isCanceled = true; @@ -576,20 +568,20 @@ const OpaAppStateOverview = ({ } break; } - } }; const EnvVars = ({ appID }: { appID: string }) => { - const envVarArr = getDeploymentEnvVars(appID); if (envVarArr && envVarArr.length) { return ( <> - {envVarArr.map((envVar) => ( + {envVarArr.map(envVar => ( - {envVar.key} + + {envVar.key} + {envVar.value} ))} @@ -598,60 +590,87 @@ const OpaAppStateOverview = ({ } else { return ( - None configured - + + None configured + + ); } }; - const DeploymentCard = ({ deploymentState, index, total }: { deploymentState: AppState, index: number, total: number }) => { - + const DeploymentCard = ({ + deploymentState, + index, + total, + }: { + deploymentState: AppState; + index: number; + total: number; + }) => { return ( <> - Deployment {index > 1 ? index : ""} + + Deployment {index > 1 ? index : ''} +
- { - deploymentState?.appState ? - ( - - - -
- - - + + + + +
- - - Name - {deploymentState.stateObject.metadata.name} - - - Status - {deploymentState?.appState ? deploymentState?.appState : 'Not Running'} - - - Pods - {deploymentState?.runningCount + "/" + deploymentState?.desiredCount}{deploymentState?.pendingCount ? ` (${deploymentState?.pendingCount} Pending)` : ''} - - - Last Updated - {deploymentState?.lastStateTimestamp ? deploymentState?.lastStateTimestamp.toString() : ''} - - -
-
-
- ) : <> - } + {deploymentState?.appState ? ( + + + + + + + + Name + + {deploymentState.stateObject.metadata.name} + + + + Status + + + {deploymentState?.appState ? deploymentState?.appState : 'Not Running'} + + + + + Pods + + + {deploymentState?.runningCount + '/' + deploymentState?.desiredCount} + {deploymentState?.pendingCount ? ` (${deploymentState?.pendingCount} Pending)` : ''} + + + + + Last Updated + + + {deploymentState?.lastStateTimestamp ? deploymentState?.lastStateTimestamp.toString() : ''} + + + +
+
+
+ ) : ( + <> + )}
- Environment Variables + + Environment Variables + - +
@@ -662,65 +681,68 @@ const OpaAppStateOverview = ({ - { - index === total && deploymentState?.appState === AppStateType.STOPPED ? - ( -
- deploymentState.desiredCount = val || 0} - min={0} - max={10} - slots={{ - root: StyledInputRoot, - input: StyledInput, - incrementButton: StyledButton, - decrementButton: StyledButton, - }} - slotProps={{ - incrementButton: { - children: , - className: 'increment', - }, - decrementButton: { - children: , - }, - }} - /> -
) : - <> - } - { - index === total ? - ( - <> - - - **Changes to your application state will be applied directly to the cluster and not to the source code repository - ) : <> - } + {index === total && deploymentState?.appState === AppStateType.STOPPED ? ( +
+ (deploymentState.desiredCount = val || 0)} + min={0} + max={10} + slots={{ + root: StyledInputRoot, + input: StyledInput, + incrementButton: StyledButton, + decrementButton: StyledButton, + }} + slotProps={{ + incrementButton: { + children: , + className: 'increment', + }, + decrementButton: { + children: , + }, + }} + /> +
+ ) : ( + <> + )} + {index === total ? ( + <> + + + + {' '} + **Changes to your application state will be applied directly to the cluster and not to the source code + repository + + + ) : ( + <> + )}
- ) - } + ); + }; if (loading) { return ( @@ -739,19 +761,25 @@ const OpaAppStateOverview = ({ - Cluster Info + + Cluster Info + -
+
- - Cluster Name - {clusterNameState} + + + Cluster Name + + {clusterNameState} - - Namespace - {env.app.namespace} + + + Namespace + + {env.app.namespace}
@@ -760,12 +788,20 @@ const OpaAppStateOverview = ({
- { - appStateData.length ? - appStateData.map((state, index, array) => { - return () - }) : <>No Deployments Found - } + {appStateData.length ? ( + appStateData.map((state, index, array) => { + return ( + + ); + }) + ) : ( + <>No Deployments Found + )} @@ -777,22 +813,27 @@ export const K8sAppStateCard = () => { const awsAppLoadingStatus = useAsyncAwsApp(); if (awsAppLoadingStatus.loading) { - return + return ; } else if (awsAppLoadingStatus.component) { let input; - if (awsAppLoadingStatus.component.componentSubType === "aws-eks") { + if (awsAppLoadingStatus.component.componentSubType === 'aws-eks') { const env = awsAppLoadingStatus.component.currentEnvironment as AWSEKSAppDeploymentEnvironment; input = { env, entity, - awsComponent: awsAppLoadingStatus.component + awsComponent: awsAppLoadingStatus.component, }; - return + return ; } else { - return + return ( + + ); } - } else { - return + return ; } }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/LabelTable/LabelTable.tsx b/backstage-plugins/plugins/aws-apps/src/components/LabelTable/LabelTable.tsx index 282ce66f..0d6c9dcf 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/LabelTable/LabelTable.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/LabelTable/LabelTable.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { } from 'react'; +import React from 'react'; import { EmptyState } from '@backstage/core-components'; import { GenericTable } from '../GenericTable/GenericTable'; import { useEntity } from '@backstage/plugin-catalog-react'; diff --git a/backstage-plugins/plugins/aws-apps/src/components/ProviderInfoCard/ProviderInfoCard.tsx b/backstage-plugins/plugins/aws-apps/src/components/ProviderInfoCard/ProviderInfoCard.tsx index 7249188b..e5482d97 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/ProviderInfoCard/ProviderInfoCard.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/ProviderInfoCard/ProviderInfoCard.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useEntity, } from '@backstage/plugin-catalog-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { InfoCard, Table, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; @@ -35,74 +35,74 @@ const ProviderInfo = (props: ProviderInfoProps) => { }, ]; - let items: KeyValue[] = [] + let items: KeyValue[] = []; items.push({ - key: "Prefix", - value: metadata['prefix']?.toString() || "" + key: 'Prefix', + value: metadata['prefix']?.toString() || '', }); items.push({ - key: "Name", - value: metadata.name.toString() || "" + key: 'Name', + value: metadata.name.toString() || '', }); items.push({ - key: "AWS Account", - value: metadata['awsAccount']?.toString() || "" + key: 'AWS Account', + value: metadata['awsAccount']?.toString() || '', }); items.push({ - key: "AWS Region", - value: metadata['awsRegion']?.toString() || "" + key: 'AWS Region', + value: metadata['awsRegion']?.toString() || '', }); items.push({ - key: "Runtime", - value: metadata['envType']?.toString() || "" + key: 'Runtime', + value: metadata['envType']?.toString() || '', }); items.push({ - key: "Audit Table", - value: metadata['auditTable']?.toString() || "" + key: 'Audit Table', + value: metadata['auditTable']?.toString() || '', }); items.push({ - key: "VPC", - value: metadata['vpc']?.toString() || "" + key: 'VPC', + value: metadata['vpc']?.toString() || '', }); - const envType = metadata['envType']?.toString() || ""; + const envType = metadata['envType']?.toString() || ''; if (envType === ProviderType.ECS || envType === ProviderType.EKS) { items.push({ - key: "Cluster Name", - value: metadata['clusterName']?.toString() || "" + key: 'Cluster Name', + value: metadata['clusterName']?.toString() || '', }); } if (envType === ProviderType.EKS) { items.push({ - key: "Node Type", - value: metadata['nodeType']?.toString() || "" + key: 'Node Type', + value: metadata['nodeType']?.toString() || '', }); } items.push({ - key: "Operation Role", - value: metadata['operationRole']?.toString() || "" + key: 'Operation Role', + value: metadata['operationRole']?.toString() || '', }); items.push({ - key: "Provisioning Role", - value: metadata['provisioningRole']?.toString() || "" + key: 'Provisioning Role', + value: metadata['provisioningRole']?.toString() || '', }); if (envType === ProviderType.EKS) { items.push({ - key: "Cluster Admin Role ARN", - value: metadata['clusterAdminRole']?.toString() || "" + key: 'Cluster Admin Role ARN', + value: metadata['clusterAdminRole']?.toString() || '', }); items.push({ - key: "API Endpoint Access", - value: metadata['apiAccess']?.toString() || "" + key: 'API Endpoint Access', + value: metadata['apiAccess']?.toString() || '', }); items.push({ - key: "Kubectl / Helm Lambda ARN", - value: metadata['kubectlLambdaArn']?.toString() || "" + key: 'Kubectl / Helm Lambda ARN', + value: metadata['kubectlLambdaArn']?.toString() || '', }); items.push({ - key: "Kubectl / Helm Lambda Role ARN", - value: metadata['kubectlLambdaAssumeRoleArn']?.toString() || "" + key: 'Kubectl / Helm Lambda Role ARN', + value: metadata['kubectlLambdaAssumeRoleArn']?.toString() || '', }); } @@ -116,7 +116,7 @@ const ProviderInfo = (props: ProviderInfoProps) => { showTitle: false, header: false, filtering: false, - toolbar: false + toolbar: false, }} data={items} columns={columns} diff --git a/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceBinding.tsx b/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceBinding.tsx index 53e486aa..60eabcbf 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceBinding.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceBinding.tsx @@ -2,14 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useState } from 'react'; -import { EmptyState, InfoCard, } from '@backstage/core-components'; -import { Button, IconButton, LinearProgress, TableBody, TableCell, TableRow, Table, TableHead, CardContent, Grid } from '@material-ui/core'; +import { EmptyState, InfoCard } from '@backstage/core-components'; +import { + Button, + IconButton, + LinearProgress, + TableBody, + TableCell, + TableRow, + Table, + TableHead, + CardContent, + Grid, +} from '@material-ui/core'; import DeleteIcon from '@mui/icons-material/Delete'; import { useApi } from '@backstage/core-plugin-api'; import { opaApiRef } from '../../api'; import { Alert, AlertTitle, Typography } from '@mui/material'; import { useAsyncAwsApp } from '../../hooks/useAwsApp'; -import { AWSComponent, AssociatedResources, BindResourceParams, ResourceBinding, ResourcePolicy, getGitCredentailsSecret } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSComponent, + AssociatedResources, + BindResourceParams, + ResourceBinding, + ResourcePolicy, + getGitCredentailsSecret, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { CompoundEntityRef, Entity, EntityRelation, parseEntityRef } from '@backstage/catalog-model'; import { CatalogApi, EntityRefLink, catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; import { ResourceSelectorDialog } from './ResourceSelectorDialog'; @@ -47,180 +65,171 @@ const ResourceBindingCard = ({ const [openDialog, setOpenDialog] = useState(false); const [spinning, setSpinning] = useState(false); const [isBindSuccessful, setIsBindSuccessful] = useState(false); - const [bindResourceMessage, setBindResourceMessage] = useState(""); + const [bindResourceMessage, setBindResourceMessage] = useState(''); const [bindResourceRequest, setBindResourceRequest] = useState(); const repoInfo = awsComponent.getRepoInfo(); useEffect(() => { - getBindingDetails() - + getBindingDetails(); }, []); async function getBindingDetails() { // find existing resource relationships const resourceRefs: EntityRelation[] | undefined = entity.relations?.filter( - relation => parseEntityRef(relation?.targetRef).kind === 'resource')!; + relation => parseEntityRef(relation?.targetRef).kind === 'resource', + )!; - const resourcesEntities = await Promise.all(resourceRefs.map(async (entityRef: { targetRef: string | CompoundEntityRef; }) => { - const entity = await catalog.getEntityByRef(entityRef.targetRef); - return entity; - })); + const resourcesEntities = await Promise.all( + resourceRefs.map(async (entityRef: { targetRef: string | CompoundEntityRef }) => { + const entity = await catalog.getEntityByRef(entityRef.targetRef); + return entity; + }), + ); //select view for only current environment const currentEnvironment = awsComponent.currentEnvironment.environment.name; const matchedResources = resourcesEntities.filter(entity => { - const appData = entity!.metadata["appData"] as any; - return appData && appData[currentEnvironment] - }) + const appData = entity!.metadata['appData'] as any; + return appData && appData[currentEnvironment]; + }); let resources: ResourceBinding[] = []; matchedResources.forEach(et => { - const appData = et!.metadata["appData"] as any; + const appData = et!.metadata['appData'] as any; const envAppData = appData[currentEnvironment] as any; - const providers = Object.keys(envAppData) + const providers = Object.keys(envAppData); providers.forEach(p => { const providerAppData = envAppData[p] as any; - if (et!.metadata['resourceType'] === "aws-rds") { - - const associatedRDSResources: AssociatedResources = - { + if (et!.metadata['resourceType'] === 'aws-rds') { + const associatedRDSResources: AssociatedResources = { resourceArn: providerAppData['DbAdminSecretArn'], - resourceType: "aws-db-secret", - resourceName: `${et!.metadata.name}-secret` - } - - resources.push( - { - resourceName: et!.metadata.name, - resourceType: et!.metadata['resourceType']?.toString() || "", - provider: p, - resourceArn: providerAppData['Arn'], - id: providerAppData['Arn'], - entityRef: "resource:default/" + et!.metadata.name, - associatedResources: [associatedRDSResources] - }) - } - else { - resources.push( - { - resourceName: et!.metadata.name, - resourceType: et!.metadata['resourceType']?.toString() || "", - provider: p, - resourceArn: providerAppData['Arn'], - id: providerAppData['Arn'], - entityRef: "resource:default/" + et!.metadata.name - } - ) + resourceType: 'aws-db-secret', + resourceName: `${et!.metadata.name}-secret`, + }; + + resources.push({ + resourceName: et!.metadata.name, + resourceType: et!.metadata['resourceType']?.toString() || '', + provider: p, + resourceArn: providerAppData['Arn'], + id: providerAppData['Arn'], + entityRef: 'resource:default/' + et!.metadata.name, + associatedResources: [associatedRDSResources], + }); + } else { + resources.push({ + resourceName: et!.metadata.name, + resourceType: et!.metadata['resourceType']?.toString() || '', + provider: p, + resourceArn: providerAppData['Arn'], + id: providerAppData['Arn'], + entityRef: 'resource:default/' + et!.metadata.name, + }); } - }) - }) + }); + }); // console.log(resources) - setItems(resources) - + setItems(resources); } async function bindResource(item: ResourceBinding): Promise { let policies: ResourcePolicy[] = []; - if (item.resourceType === "aws-rds") { - const rdsPolicy = RDS_POLICY.replace("@@@PLACEHOLDER@@@", item.resourceArn); + if (item.resourceType === 'aws-rds') { + const rdsPolicy = RDS_POLICY.replace('@@@PLACEHOLDER@@@', item.resourceArn); policies.push({ policyFileName: `statement-rds-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, policyContent: rdsPolicy, - policyResource: item.resourceType + policyResource: item.resourceType, }); if (item.associatedResources && item.associatedResources.length > 0) { item.associatedResources.forEach(ar => { - if (ar.resourceType === "aws-db-secret") { - const secretPolicy = SECRET_POLICY.replace("@@@PLACEHOLDER@@@", ar.resourceArn); + if (ar.resourceType === 'aws-db-secret') { + const secretPolicy = SECRET_POLICY.replace('@@@PLACEHOLDER@@@', ar.resourceArn); policies.push({ policyFileName: `statement-secrets-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, policyContent: secretPolicy, - policyResource: item.resourceType + policyResource: item.resourceType, }); } - }) + }); } - } else if (item.resourceType === "aws-db-secret") { - const secretPolicy = SECRET_POLICY.replace("@@@PLACEHOLDER@@@", item.resourceArn); + } else if (item.resourceType === 'aws-db-secret') { + const secretPolicy = SECRET_POLICY.replace('@@@PLACEHOLDER@@@', item.resourceArn); policies.push({ policyFileName: `statement-secrets-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, policyContent: secretPolicy, - policyResource: item.resourceType + policyResource: item.resourceType, }); - } - else if (item.resourceType === "aws-secretsmanager") { - const secretPolicy = SECRET_POLICY.replace("@@@PLACEHOLDER@@@", item.resourceArn); + } else if (item.resourceType === 'aws-secretsmanager') { + const secretPolicy = SECRET_POLICY.replace('@@@PLACEHOLDER@@@', item.resourceArn); policies.push({ policyFileName: `statement-secrets-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, policyContent: secretPolicy, - policyResource: item.resourceType + policyResource: item.resourceType, }); - } else if (item.resourceType === "aws-s3") { - const s3Policy = S3_POLICY.replace("@@@PLACEHOLDER@@@", `"${item.resourceArn}","${item.resourceArn}/*"`); + } else if (item.resourceType === 'aws-s3') { + const s3Policy = S3_POLICY.replace('@@@PLACEHOLDER@@@', `"${item.resourceArn}","${item.resourceArn}/*"`); policies.push({ policyFileName: `statement-s3-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, policyContent: s3Policy, - policyResource: item.resourceType + policyResource: item.resourceType, }); } - const params: BindResourceParams = { providerName: item.provider, envName: awsComponent.currentEnvironment.environment.name, appName: entity.metadata.name, resourceName: item.resourceName, resourceEntityRef: item.id, - policies + policies, }; - return api.bindResource({ repoInfo, params, gitAdminSecret: getGitCredentailsSecret(repoInfo) }) + return api.bindResource({ repoInfo, params, gitAdminSecret: getGitCredentailsSecret(repoInfo) }); } async function removeResource(item: ResourceBinding): Promise { let policies: ResourcePolicy[] = []; - if (item.resourceType === "aws-rds") { + if (item.resourceType === 'aws-rds') { policies.push({ policyFileName: `statement-rds-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, - policyContent: "", - policyResource: item.resourceType + policyContent: '', + policyResource: item.resourceType, }); if (item.associatedResources && item.associatedResources.length > 0) { item.associatedResources.forEach(ar => { - if (ar.resourceType === "aws-db-secret") { + if (ar.resourceType === 'aws-db-secret') { policies.push({ policyFileName: `statement-secrets-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, - policyContent: "", - policyResource: item.resourceType + policyContent: '', + policyResource: item.resourceType, }); } - }) + }); } - } else if (item.resourceType === "aws-db-secret") { + } else if (item.resourceType === 'aws-db-secret') { policies.push({ policyFileName: `statement-secrets-${awsComponent.currentEnvironment.environment.name}-${item.provider}-${item.resourceName}`, - policyContent: "", - policyResource: item.resourceType + policyContent: '', + policyResource: item.resourceType, }); } const params: BindResourceParams = { - providerName: item.provider, envName: awsComponent.currentEnvironment.environment.name, appName: entity.metadata.name, resourceName: item.resourceName, resourceEntityRef: item.entityRef!, - policies + policies, }; - return api.unBindResource({ repoInfo, params, gitAdminSecret: getGitCredentailsSecret(repoInfo) }) + return api.unBindResource({ repoInfo, params, gitAdminSecret: getGitCredentailsSecret(repoInfo) }); } - const handleClickAdd = async () => { setOpenDialog(true); }; @@ -228,46 +237,49 @@ const ResourceBindingCard = ({ const closeDialog = () => setOpenDialog(false); const selectResourceHandler = (item: ResourceBinding) => { - setSpinning(true) - setBindResourceRequest(item) - bindResource(item).then(results => { - setBindResourceMessage(results.message) - setIsBindSuccessful(true) - - setSpinning(false) - }).catch(err => { - setIsBindSuccessful(false) - setBindResourceMessage(err) - console.log(err); - setError(err) - setSpinning(false) - }) - - } + setSpinning(true); + setBindResourceRequest(item); + bindResource(item) + .then(results => { + setBindResourceMessage(results.message); + setIsBindSuccessful(true); + + setSpinning(false); + }) + .catch(err => { + setIsBindSuccessful(false); + setBindResourceMessage(err); + console.log(err); + setError(err); + setSpinning(false); + }); + }; const deleteClick = async (index: number) => { - const deletedItem = items.at(index) - setSpinning(true) - removeResource(deletedItem!).then(results => { - setBindResourceMessage(results.message) - setIsBindSuccessful(true) - - //remove from table - const resourceData = items.slice(); - resourceData.splice(index, 1); - setItems(resourceData); - setSpinning(false) - }).catch(err => { - setIsBindSuccessful(false) - setBindResourceMessage(err) - console.log(err); - setError(err) - setSpinning(false) - }) - } + const deletedItem = items.at(index); + setSpinning(true); + removeResource(deletedItem!) + .then(results => { + setBindResourceMessage(results.message); + setIsBindSuccessful(true); + + //remove from table + const resourceData = items.slice(); + resourceData.splice(index, 1); + setItems(resourceData); + setSpinning(false); + }) + .catch(err => { + setIsBindSuccessful(false); + setBindResourceMessage(err); + console.log(err); + setError(err); + setSpinning(false); + }); + }; const handleCloseAlert = () => { - setBindResourceMessage(""); + setBindResourceMessage(''); }; const deleteIcon = (index: number) => ( @@ -294,19 +306,24 @@ const ResourceBindingCard = ({ - { - items.map((record, index) => { - return ( - - {record.id} - - - {record.resourceType} - {deleteIcon(index)} - - ) - }) - } + {items.map((record, index) => { + return ( + + {record.id} + + + + + {' '} + + {record.resourceType} + {deleteIcon(index)} + + ); + })} @@ -314,25 +331,43 @@ const ResourceBindingCard = ({ Success {bindResourceRequest?.resourceName!} was successfully scheduled! - {!!bindResourceMessage && (<>

{bindResourceMessage})} + {!!bindResourceMessage && ( + <> +
+
+ {bindResourceMessage} + + )}
)} {!isBindSuccessful && !!bindResourceMessage && ( Error Failed to schedule {bindResourceRequest?.resourceName!} Binding. - {!!bindResourceMessage && (<>

{bindResourceMessage})} + {!!bindResourceMessage && ( + <> +
+
+ {bindResourceMessage} + + )}
)}
- - + + - theme.zIndex.drawer + 1 }} - open={spinning} - > + theme.zIndex.drawer + 1 }} open={spinning}> @@ -351,10 +386,16 @@ export const ResourceBindingCardWidget = () => { const input = { awsComponent: awsAppLoadingStatus.component, entity, - catalog: catalogApi + catalog: catalogApi, }; return ; } else { - return ; + return ( + + ); } }; diff --git a/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceSelectorDialog.tsx b/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceSelectorDialog.tsx index b38232ac..fb87499c 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceSelectorDialog.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/ResourceBindingCard/ResourceSelectorDialog.tsx @@ -8,8 +8,16 @@ import { Dialog, DialogActions, DialogContent, - DialogTitle, Grid, - IconButton, makeStyles, Radio, Table, TableBody, TableCell, TableHead, TableRow + DialogTitle, + Grid, + IconButton, + makeStyles, + Radio, + Table, + TableBody, + TableCell, + TableHead, + TableRow, } from '@material-ui/core'; import { Close } from '@mui/icons-material'; import React, { useEffect, useState } from 'react'; @@ -46,7 +54,7 @@ const useStyles = makeStyles(theme => ({ */ const ResourceSelectorTable = ({ tableData, - selectedRowCallback + selectedRowCallback, }: { tableData: ResourceBinding[]; selectedRowCallback: (item: ResourceBinding) => void; @@ -55,9 +63,9 @@ const ResourceSelectorTable = ({ const [selectedRadio, setSelectedRadio] = useState(); const selectedRow = (item: ResourceBinding, index: number) => { - selectedRowCallback(item) - setSelectedRadio(index) - } + selectedRowCallback(item); + setSelectedRadio(index); + }; return ( @@ -71,11 +79,15 @@ const ResourceSelectorTable = ({ - {tableData.map((row, index) => ( - selectedRow(row, index)} /> + selectedRow(row, index)} + /> {row.resourceName} {row.resourceType} @@ -83,7 +95,6 @@ const ResourceSelectorTable = ({ {row.resourceArn} ))} -
); @@ -102,7 +113,7 @@ export const ResourceSelectorDialog = ({ selectHandler, catalog, currentEnvironment, - associatedResources + associatedResources, }: { isOpen: boolean; closeDialogHandler: () => void; @@ -111,8 +122,6 @@ export const ResourceSelectorDialog = ({ currentEnvironment: string; associatedResources: ResourceBinding[]; }) => { - - const classes = useStyles(); // @ts-ignore const [loading, setLoading] = useState(true); @@ -127,98 +136,91 @@ export const ResourceSelectorDialog = ({ selectHandler(selectedResource); } closeDialogHandler(); - } + }; const rowSelectedHandler = (item: ResourceBinding) => { - setSelectedResource(item) - } + setSelectedResource(item); + }; const isResourceAlreadyBind = (resourceArn: string, associatedResources: ResourceBinding[]) => { - let result: boolean = false + let result: boolean = false; associatedResources.forEach(r => { if (r.resourceArn === resourceArn) { result = true; } - }) - return result - } + }); + return result; + }; async function getData() { - const tableData: ResourceBinding[] = [] + const tableData: ResourceBinding[] = []; // search the catalog for resources within the same environment and provider - const allResources = await catalog.getEntities({ filter: { 'kind': "resource", 'spec.type': 'aws-resource' } }); + const allResources = await catalog.getEntities({ filter: { kind: 'resource', 'spec.type': 'aws-resource' } }); const matchedResources = allResources.items.filter(entity => { - const appData = entity.metadata["appData"] as any; - return appData && appData[currentEnvironment] && entity.metadata.name - }) + const appData = entity.metadata['appData'] as any; + return appData && appData[currentEnvironment] && entity.metadata.name; + }); matchedResources.forEach(et => { const etNamespace = et.metadata.namespace || 'default'; const etName = et.metadata.name; const id = `resource:${etNamespace}/${etName}`; - const appData = et.metadata["appData"] as any; + const appData = et.metadata['appData'] as any; const envAppData = appData[currentEnvironment] as any; // find all providers - for multi providers - const providers = Object.keys(envAppData) + const providers = Object.keys(envAppData); providers.forEach(p => { const providerAppData = envAppData[p] as any; if (isResourceAlreadyBind(providerAppData['Arn'], associatedResources)) { return; } - if (et.metadata['resourceType'] === "aws-rds") { + if (et.metadata['resourceType'] === 'aws-rds') { //Handler for aws-rds with associated resources - const associatedRDSResources: AssociatedResources = - { + const associatedRDSResources: AssociatedResources = { resourceArn: providerAppData['DbAdminSecretArn'], - resourceType: "aws-db-secret", - resourceName: `${etName}-secret` - } + resourceType: 'aws-db-secret', + resourceName: `${etName}-secret`, + }; - tableData.push( - { - resourceName: etName, - resourceType: et.metadata['resourceType']?.toString() || "", - provider: p, - resourceArn: providerAppData['Arn'], - id, - associatedResources: [associatedRDSResources] - }) - } - else if (et.metadata['resourceType'] === "aws-s3") { - // Custom S3 bucket resource handler - add resource policy - const associatedS3Resources: AssociatedResources = - { + tableData.push({ + resourceName: etName, + resourceType: et.metadata['resourceType']?.toString() || '', + provider: p, + resourceArn: providerAppData['Arn'], + id, + associatedResources: [associatedRDSResources], + }); + } else if (et.metadata['resourceType'] === 'aws-s3') { + // Custom S3 bucket resource handler - add resource policy + const associatedS3Resources: AssociatedResources = { resourceArn: providerAppData['Arn'], - resourceType: "aws-s3", - resourceName: `${etName}-secret` - } + resourceType: 'aws-s3', + resourceName: `${etName}-secret`, + }; - tableData.push( - { - resourceName: etName, - resourceType: et.metadata['resourceType']?.toString() || "", - provider: p, - resourceArn: providerAppData['Arn'], - id, - associatedResources: [associatedS3Resources] - }) - } - else { + tableData.push({ + resourceName: etName, + resourceType: et.metadata['resourceType']?.toString() || '', + provider: p, + resourceArn: providerAppData['Arn'], + id, + associatedResources: [associatedS3Resources], + }); + } else { // General AWS resource handler - tableData.push( - { - resourceName: etName, - resourceType: et.metadata['resourceType']?.toString() || "", - provider: p, - resourceArn: providerAppData['Arn'], - id, - }) + tableData.push({ + resourceName: etName, + resourceType: et.metadata['resourceType']?.toString() || '', + provider: p, + resourceArn: providerAppData['Arn'], + id, + }); } - }) - }) - setTableData(tableData) + }); + }); + setTableData(tableData); } useEffect(() => { @@ -255,4 +257,3 @@ export const ResourceSelectorDialog = ({ ); }; - diff --git a/backstage-plugins/plugins/aws-apps/src/components/common/SecretStringComponent.tsx b/backstage-plugins/plugins/aws-apps/src/components/common/SecretStringComponent.tsx index cd46fedc..06bb39a5 100644 --- a/backstage-plugins/plugins/aws-apps/src/components/common/SecretStringComponent.tsx +++ b/backstage-plugins/plugins/aws-apps/src/components/common/SecretStringComponent.tsx @@ -1,9 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - IconButton -} from '@material-ui/core'; +import { IconButton } from '@material-ui/core'; import { Visibility, VisibilityOff } from '@mui/icons-material'; import React, { useState } from 'react'; diff --git a/backstage-plugins/plugins/aws-apps/src/helpers/constants.ts b/backstage-plugins/plugins/aws-apps/src/helpers/constants.ts index aca63cb2..c068a9a4 100644 --- a/backstage-plugins/plugins/aws-apps/src/helpers/constants.ts +++ b/backstage-plugins/plugins/aws-apps/src/helpers/constants.ts @@ -1,18 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StackStatus } from "@aws-sdk/client-cloudformation"; +import { StackStatus } from '@aws-sdk/client-cloudformation'; export enum ExtraStackDeployStatus { - STAGED = "STAGED", - UNSTAGED = "UNSTAGED" + STAGED = 'STAGED', + UNSTAGED = 'UNSTAGED', } export enum ProviderType { - ECS = "ecs", - EKS = "eks", - SERVERLESS = "serverless", - GENAI_SERVERLESS = "gen-ai-serverless" + ECS = 'ecs', + EKS = 'eks', + SERVERLESS = 'serverless', + GENAI_SERVERLESS = 'gen-ai-serverless', } export type DeployStackStatus = StackStatus | ExtraStackDeployStatus; diff --git a/backstage-plugins/plugins/aws-apps/src/helpers/date-utils.ts b/backstage-plugins/plugins/aws-apps/src/helpers/date-utils.ts index 4f508c89..3831e317 100644 --- a/backstage-plugins/plugins/aws-apps/src/helpers/date-utils.ts +++ b/backstage-plugins/plugins/aws-apps/src/helpers/date-utils.ts @@ -6,5 +6,5 @@ export function formatWithTime(date: Date): string { const month = date.getMonth() + 1; const year = date.getFullYear(); const zone = date.toString().slice(date.toString().lastIndexOf('(')); - return `${month}/${day}/${year} ${date.toLocaleTimeString()} ${zone}` -} + return `${month}/${day}/${year} ${date.toLocaleTimeString()} ${zone}`; +} diff --git a/backstage-plugins/plugins/aws-apps/src/helpers/util.ts b/backstage-plugins/plugins/aws-apps/src/helpers/util.ts index 40e70492..24dfc8e6 100644 --- a/backstage-plugins/plugins/aws-apps/src/helpers/util.ts +++ b/backstage-plugins/plugins/aws-apps/src/helpers/util.ts @@ -3,10 +3,10 @@ export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); -export const base64PayloadConvert = (payload:Object) => { - let str = ""; - Object.values(payload).forEach(k=> { - str+=String.fromCharCode(k) - }) - return str; -} \ No newline at end of file +export const base64PayloadConvert = (payload: Object) => { + let str = ''; + Object.values(payload).forEach(k => { + str += String.fromCharCode(k); + }); + return str; +}; diff --git a/backstage-plugins/plugins/aws-apps/src/hooks/useAwsApp.tsx b/backstage-plugins/plugins/aws-apps/src/hooks/useAwsApp.tsx index 07b07622..3bb4e43b 100644 --- a/backstage-plugins/plugins/aws-apps/src/hooks/useAwsApp.tsx +++ b/backstage-plugins/plugins/aws-apps/src/hooks/useAwsApp.tsx @@ -3,15 +3,26 @@ import React, { ReactNode, createContext } from 'react'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; -import { - catalogApiRef, - useEntity, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { Entity, EntityRelation, parseEntityRef } from '@backstage/catalog-model'; // TODO: consider moving AWSEnvironmentEntityV1 AWSEnvironmentProviderEntityV1 to common plugin import { AWSEnvironmentEntityV1, AWSEnvironmentProviderEntityV1 } from '@aws/plugin-aws-apps-common-for-backstage'; -import { AWSComponent, AWSComponentType, AWSDeploymentEnvironment, AWSECSAppDeploymentEnvironment, AWSEKSAppDeploymentEnvironment, AWSResourceDeploymentEnvironment, AWSServerlessAppDeploymentEnvironment, AwsDeploymentEnvironments, CloudFormationStack, ComponentStateType, GenericAWSEnvironment, IRepositoryInfo, getRepoInfo } from '@aws/plugin-aws-apps-common-for-backstage'; +import { + AWSComponent, + AWSComponentType, + AWSDeploymentEnvironment, + AWSECSAppDeploymentEnvironment, + AWSEKSAppDeploymentEnvironment, + AWSResourceDeploymentEnvironment, + AWSServerlessAppDeploymentEnvironment, + AwsDeploymentEnvironments, + CloudFormationStack, + ComponentStateType, + GenericAWSEnvironment, + IRepositoryInfo, + getRepoInfo, +} from '@aws/plugin-aws-apps-common-for-backstage'; import { ProviderType, ExtraStackDeployStatus, DeployStackStatus } from '../helpers/constants'; import { opaApiRef } from '../api'; import { formatWithTime } from '../helpers/date-utils'; @@ -31,14 +42,14 @@ export type AwsComponentHookLoadingStatus = { type EntityNameAndRef = { envName: string; targetRef: string; -} +}; type EntityLookupResponse = EntityNameAndRef & { entity: Entity | undefined; -} +}; type MultiEntityNameAndRef = { envName: string; targetRefs: string[]; -} +}; // Private / internal variables let _app_name: string | null = null; @@ -55,34 +66,33 @@ const _setCurrentProvider = (envName: string, providerName: string) => { _refresh(); // calls getData function }; -/** +/** * Loads AWS App deployment data and returns response that will allow * consumers to get the data and see whether it is still being loaded * or if there was an error loading it. * @public -*/ + */ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { - const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); const config = useApi(configApiRef); const api = useApi(opaApiRef); api.setPlatformParams(entity.metadata.name, config.getString('backend.platformRegion')); - if (entity.metadata.name != _app_name) - { + if (entity.metadata.name != _app_name) { // switched app reset references of drop down env selector _envProviderEntityMap = null; _change_to_env_name = null; _change_to_env_provider_name = null; } - _app_name=entity.metadata.name + _app_name = entity.metadata.name; // Look up environment and environment provider entities from the catalog async function getCatalogData(): Promise { if (!entity.relations) { throw new Error(`Entity ${entity.metadata.name} has no relations`); } - const envRefs: EntityRelation[] = entity.relations - .filter(relation => parseEntityRef(relation.targetRef).kind === 'awsenvironment') + const envRefs: EntityRelation[] = entity.relations.filter( + relation => parseEntityRef(relation.targetRef).kind === 'awsenvironment', + ); if (!envRefs.length) { throw new Error(`Entity ${entity.metadata.name} does not have an AWS environment.`); } @@ -95,21 +105,21 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { envName: envEntity?.metadata.name || parseEntityRef(relation.targetRef).name, targetRef: relation.targetRef, entity: envEntity, - } - }) + }; + }), ); // Get the targetRef for each environment's providers const envProviderRefs = envEntities .filter(envLookupResponse => !!envLookupResponse.entity?.relations) .map(envLookupResponse => { - const envProviderRefs: EntityRelation[] | undefined = - envLookupResponse.entity?.relations?.filter( - relation => parseEntityRef(relation?.targetRef).kind === 'awsenvironmentprovider') + const envProviderRefs: EntityRelation[] | undefined = envLookupResponse.entity?.relations?.filter( + relation => parseEntityRef(relation?.targetRef).kind === 'awsenvironmentprovider', + ); return { envName: envLookupResponse.envName, - targetRefs: envProviderRefs ? envProviderRefs.map(ref => ref.targetRef) : [] - } + targetRefs: envProviderRefs ? envProviderRefs.map(ref => ref.targetRef) : [], + }; }) .filter(envProviderMap => !!envProviderMap.targetRefs.length) .reduce((acc: EntityNameAndRef[], envProviderEntities: MultiEntityNameAndRef) => { @@ -122,20 +132,23 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { // Get a map of environment provider entities. The map key is the environment name. The // map value is an array of provider entities for that environment. Note, each environment // can have more than one provider entity in a multi-region or multi-account scenario. - const envProviderEntityMap: EnvEntityMap = (await Promise.all( - envProviderRefs.map(async (providerRef): Promise => { - const envProviderEntity = await catalogApi.getEntityByRef(providerRef.targetRef); - return { - envName: providerRef.envName, - targetRef: providerRef.targetRef, - entity: envProviderEntity, - } - }) - )).reduce((acc, envProviderEntity) => { + const envProviderEntityMap: EnvEntityMap = ( + await Promise.all( + envProviderRefs.map(async (providerRef): Promise => { + const envProviderEntity = await catalogApi.getEntityByRef(providerRef.targetRef); + return { + envName: providerRef.envName, + targetRef: providerRef.targetRef, + entity: envProviderEntity, + }; + }), + ) + ).reduce((acc, envProviderEntity) => { const typedAcc: EnvEntityMap = acc; if (!typedAcc[envProviderEntity.envName]) { return { - ...typedAcc, [envProviderEntity.envName]: [envProviderEntity.entity] + ...typedAcc, + [envProviderEntity.envName]: [envProviderEntity.entity], }; } else { typedAcc[envProviderEntity.envName].push(envProviderEntity.entity!); @@ -151,66 +164,69 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { } function getProviderAppData(deployEnv: AWSDeploymentEnvironment): any { - const appData = entity.metadata["appData"] as any; + const appData = entity.metadata['appData'] as any; const envAppData = appData[deployEnv.environment.name] as any; return envAppData[deployEnv.providerData.name] as any; } async function populateServerlessState(svAppDeployEnv: AWSServerlessAppDeploymentEnvironment): Promise { - let defaultDeployStatus = ExtraStackDeployStatus.UNSTAGED; const providerAppData = getProviderAppData(svAppDeployEnv); - const bucketName = providerAppData["BuildBucketName"]; + const bucketName = providerAppData['BuildBucketName']; try { await api.doesS3FileExist({ bucketName, fileName: 'packaged.yaml' }); defaultDeployStatus = ExtraStackDeployStatus.STAGED; // console.log('S3 HEAD response'); // console.log(JSON.stringify(templateFileExistsResponse, null, 2)); - } catch (e) { console.error(e); } const appStack: CloudFormationStack = { - stackName: providerAppData["AppStackName"] || "", + stackName: providerAppData['AppStackName'] || '', stackDeployStatus: defaultDeployStatus, - } + }; svAppDeployEnv.app = { logGroupNames: [], - resourceGroupArn: providerAppData["AppResourceGroup"] || "", - cloudFormationStackName: providerAppData["StackName"] || "", + resourceGroupArn: providerAppData['AppResourceGroup'] || '', + cloudFormationStackName: providerAppData['StackName'] || '', appStack, s3BucketName: bucketName, - links: [] - } + links: [], + }; try { - if (defaultDeployStatus === ExtraStackDeployStatus.STAGED) { const stackDetails = await api.getStackDetails({ stackName: appStack.stackName }); // console.log("Stack Details"); // console.log(JSON.stringify(stackDetails, null, 2)); appStack.stackDeployStatus = (stackDetails.StackStatus || ExtraStackDeployStatus.STAGED) as DeployStackStatus; - appStack.creationTime = stackDetails.CreationTime ? formatWithTime(new Date(stackDetails.CreationTime)) : undefined; - appStack.lastUpdatedTime = stackDetails.LastUpdatedTime ? formatWithTime(new Date(stackDetails.LastUpdatedTime)) : undefined; + appStack.creationTime = stackDetails.CreationTime + ? formatWithTime(new Date(stackDetails.CreationTime)) + : undefined; + appStack.lastUpdatedTime = stackDetails.LastUpdatedTime + ? formatWithTime(new Date(stackDetails.LastUpdatedTime)) + : undefined; if (stackDetails.Outputs) { - const logGroupsArrayOutput = stackDetails.Outputs.filter(output => output.OutputKey === 'LogGroupsArray')[0] ?? null; + const logGroupsArrayOutput = + stackDetails.Outputs.filter(output => output.OutputKey === 'LogGroupsArray')[0] ?? null; if (logGroupsArrayOutput) { svAppDeployEnv.app.logGroupNames.push(...JSON.parse(logGroupsArrayOutput.OutputValue as string)); } - const apiUrlOutput = stackDetails.Outputs.filter(output => output.OutputKey === 'ApiGatewayEndpointWithPath')[0] ?? null; + const apiUrlOutput = + stackDetails.Outputs.filter(output => output.OutputKey === 'ApiGatewayEndpointWithPath')[0] ?? null; if (apiUrlOutput) { svAppDeployEnv.app.links = [ { - title: "Go to app", + title: 'Go to app', url: apiUrlOutput.OutputValue as string, // icon: 'kind:api' - } + }, ]; } } @@ -219,73 +235,72 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { console.log('Failed to get stack. This is expected if the stack has not yet been deployed'); console.error(e); } - } function populateEcsState(ecsAppDeployEnv: AWSECSAppDeploymentEnvironment, providerEntity: Entity): void { - ecsAppDeployEnv.clusterName = providerEntity.metadata['clusterName']?.toString() || ""; + ecsAppDeployEnv.clusterName = providerEntity.metadata['clusterName']?.toString() || ''; const providerAppData = getProviderAppData(ecsAppDeployEnv); ecsAppDeployEnv.app = { - ecrArn: providerAppData["EcrRepositoryArn"] || "", - logGroupName: providerAppData["TaskLogGroup"] || "", - resourceGroupArn: providerAppData["AppResourceGroup"] || "", - serviceArn: providerAppData["EcsServiceArn"] || "", - taskDefArn: providerAppData["EcsTaskDefinitionArn"] || "", - cloudFormationStackName: providerAppData["StackName"] || "", - taskExecutionRoleArn: providerAppData["TaskExecutionRoleArn"] || "", + ecrArn: providerAppData['EcrRepositoryArn'] || '', + logGroupName: providerAppData['TaskLogGroup'] || '', + resourceGroupArn: providerAppData['AppResourceGroup'] || '', + serviceArn: providerAppData['EcsServiceArn'] || '', + taskDefArn: providerAppData['EcsTaskDefinitionArn'] || '', + cloudFormationStackName: providerAppData['StackName'] || '', + taskExecutionRoleArn: providerAppData['TaskExecutionRoleArn'] || '', links: [ { - title: "Go to app", - url: providerAppData["AlbEndpoint"] || "", + title: 'Go to app', + url: providerAppData['AlbEndpoint'] || '', // icon: 'kind:api' - } - ] - } + }, + ], + }; } function populateEksState(eksAppDeployEnv: AWSEKSAppDeploymentEnvironment, providerEntity: Entity): void { - eksAppDeployEnv.clusterName = providerEntity.metadata['clusterName']?.toString() || ""; + eksAppDeployEnv.clusterName = providerEntity.metadata['clusterName']?.toString() || ''; const providerAppData = getProviderAppData(eksAppDeployEnv); eksAppDeployEnv.app = { - appAdminRoleArn: providerAppData["AppAdminRoleArn"] as string || "", - ecrArn: providerAppData["EcrRepositoryArn"] as string || "", - namespace: providerAppData["Namespace"] as string || "", + appAdminRoleArn: (providerAppData['AppAdminRoleArn'] as string) || '', + ecrArn: (providerAppData['EcrRepositoryArn'] as string) || '', + namespace: (providerAppData['Namespace'] as string) || '', // Must match Fluent Bit configurations. See "log_group_template" setting in the EKS provider IaC. - logGroupName: providerAppData["LogGroup"] || `/aws/apps/${eksAppDeployEnv.providerData.prefix}-${eksAppDeployEnv.providerData.name}/${providerAppData["Namespace"]}`, - - resourceGroupArn: providerAppData["AppResourceGroup"] || "", - cloudFormationStackName: providerAppData["StackName"] || "", - links: [] - } + logGroupName: + providerAppData['LogGroup'] || + `/aws/apps/${eksAppDeployEnv.providerData.prefix}-${eksAppDeployEnv.providerData.name}/${providerAppData['Namespace']}`, + + resourceGroupArn: providerAppData['AppResourceGroup'] || '', + cloudFormationStackName: providerAppData['StackName'] || '', + links: [], + }; - if (providerAppData["AlbEndpoint"]) { + if (providerAppData['AlbEndpoint']) { eksAppDeployEnv.app.links.push({ - title: "Go to app", - url: providerAppData["AlbEndpoint"] || "", + title: 'Go to app', + url: providerAppData['AlbEndpoint'] || '', // icon: 'kind:api' }); } - } function populateResourceState(resourceDeployEnv: AWSResourceDeploymentEnvironment): void { - resourceDeployEnv.resource.resourceName = entity.metadata.name + resourceDeployEnv.resource.resourceName = entity.metadata.name; const providerAppData = getProviderAppData(resourceDeployEnv); - resourceDeployEnv.resource.arn = providerAppData['Arn']?.toString() || ""; - resourceDeployEnv.resource.resourceGroupArn = providerAppData['ResourceGroup']?.toString() || ""; - resourceDeployEnv.resource.cloudFormationStackName = providerAppData['StackName']?.toString() || ""; + resourceDeployEnv.resource.arn = providerAppData['Arn']?.toString() || ''; + resourceDeployEnv.resource.resourceGroupArn = providerAppData['ResourceGroup']?.toString() || ''; + resourceDeployEnv.resource.cloudFormationStackName = providerAppData['StackName']?.toString() || ''; } // Retrieve AWS App Deployment data from the back end async function getData(): Promise { - // We load catalog data if we've never loaded it before for the current // application entity or if we want to refresh the data. // We do NOT load catalog data again if we only want to switch the current @@ -293,8 +308,8 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { //TODO: Feature optimization to cache app related data - make sure the key of the app matches. multi -app-> multi-env->multi-providers.. //if (!_envEntities) { - await getCatalogData(); -// } + await getCatalogData(); + // } const envEntities = _envEntities!; const envProviderEntityMap = _envProviderEntityMap!; @@ -306,146 +321,147 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { let envProvider; if (envEntity.envName === _change_to_env_name) { - envProvider = envProviders - .filter((providerEntity: Entity) => providerEntity.metadata.name === _change_to_env_provider_name)[0]; - + envProvider = envProviders.filter( + (providerEntity: Entity) => providerEntity.metadata.name === _change_to_env_provider_name, + )[0]; + if (!envProvider) { - console.error(`Could not filter based on env name ${_change_to_env_name} and provider name ${_change_to_env_provider_name}`) + console.error( + `Could not filter based on env name ${_change_to_env_name} and provider name ${_change_to_env_provider_name}`, + ); envProvider = envProviders[0]; } - } else { envProvider = envProviders[0]; } const awsDeploymentEnvironment: AWSDeploymentEnvironment = { environment: { - accountType: envEntity.entity?.metadata['envTypeAccount']?.toString() || "", - category: envEntity.entity?.metadata['category']?.toString() || "", - classification: envEntity.entity?.metadata['classification']?.toString() || "", - description: envEntity.entity?.metadata['description']?.toString() || "", - envType: envEntity.entity?.metadata['environmentType']?.toString() || "", - level: parseInt(envEntity.entity?.metadata['level']?.toString() || "0", 10), - name: envEntity.entity?.metadata['name'].toString() || "", - regionType: envEntity.entity?.metadata['envTypeRegion']?.toString() || "", + accountType: envEntity.entity?.metadata['envTypeAccount']?.toString() || '', + category: envEntity.entity?.metadata['category']?.toString() || '', + classification: envEntity.entity?.metadata['classification']?.toString() || '', + description: envEntity.entity?.metadata['description']?.toString() || '', + envType: envEntity.entity?.metadata['environmentType']?.toString() || '', + level: parseInt(envEntity.entity?.metadata['level']?.toString() || '0', 10), + name: envEntity.entity?.metadata['name'].toString() || '', + regionType: envEntity.entity?.metadata['envTypeRegion']?.toString() || '', }, providerData: { - accountNumber: envProvider.metadata['awsAccount']?.toString() || "", - region: envProvider.metadata['awsRegion']?.toString() || "", - prefix: envProvider.metadata['prefix']?.toString() || "", - auditTable: envProvider.metadata['auditTable']?.toString() || "", - description: envProvider.metadata['description']?.toString() || "", + accountNumber: envProvider.metadata['awsAccount']?.toString() || '', + region: envProvider.metadata['awsRegion']?.toString() || '', + prefix: envProvider.metadata['prefix']?.toString() || '', + auditTable: envProvider.metadata['auditTable']?.toString() || '', + description: envProvider.metadata['description']?.toString() || '', name: envProvider.metadata['name'], - operationRoleSsmKey: envProvider.metadata['operationRole']?.toString() || "", - provisioningRoleSsmKey: envProvider.metadata['provisioningRole']?.toString() || "", - providerType: envProvider.metadata['envType']?.toString().toLowerCase() || "", - vpcSsmKey: envProvider.metadata['vpc']?.toString() || "", - cloudFormationStackName: envProvider.metadata['StackName']?.toString() || "", - terraformWorkspace: envProvider.metadata['TerraformWorkspace']?.toString() || "", - terraformStateBucket: envProvider.metadata['TerraformStateBucket']?.toString() || "", - terraformStateTable: envProvider.metadata['TerraformStateTable']?.toString() || "", + operationRoleSsmKey: envProvider.metadata['operationRole']?.toString() || '', + provisioningRoleSsmKey: envProvider.metadata['provisioningRole']?.toString() || '', + providerType: envProvider.metadata['envType']?.toString().toLowerCase() || '', + vpcSsmKey: envProvider.metadata['vpc']?.toString() || '', + cloudFormationStackName: envProvider.metadata['StackName']?.toString() || '', + terraformWorkspace: envProvider.metadata['TerraformWorkspace']?.toString() || '', + terraformStateBucket: envProvider.metadata['TerraformStateBucket']?.toString() || '', + terraformStateTable: envProvider.metadata['TerraformStateTable']?.toString() || '', }, entities: { envEntity: envEntity.entity as AWSEnvironmentEntityV1, - envProviderEntity: envProvider as AWSEnvironmentProviderEntityV1 + envProviderEntity: envProvider as AWSEnvironmentProviderEntityV1, }, app: { - cloudFormationStackName: "", // will be updated later below when app data is fetched from entity - links: [] + cloudFormationStackName: '', // will be updated later below when app data is fetched from entity + links: [], }, - resource: {} - } + resource: {}, + }; - // now adjust more specific provider data types - const providerType = envEntity.entity?.metadata['environmentType']?.toString().toLowerCase() || "N/A"; + // now adjust more specific provider data types + const providerType = envEntity.entity?.metadata['environmentType']?.toString().toLowerCase() || 'N/A'; - if (providerType === "N/A") { - throw new Error("Environment Entity not set properly - please configure environmentType"); + if (providerType === 'N/A') { + throw new Error('Environment Entity not set properly - please configure environmentType'); } - if (providerType === ProviderType.ECS && componentType === "aws-app") { + if (providerType === ProviderType.ECS && componentType === 'aws-app') { populateEcsState(awsDeploymentEnvironment as AWSECSAppDeploymentEnvironment, envProvider); - } else if (providerType === ProviderType.EKS && componentType === "aws-app") { + } else if (providerType === ProviderType.EKS && componentType === 'aws-app') { populateEksState(awsDeploymentEnvironment as AWSEKSAppDeploymentEnvironment, envProvider); - } else if (providerType === ProviderType.SERVERLESS && componentType === "aws-app") { + } else if (providerType === ProviderType.SERVERLESS && componentType === 'aws-app') { // Must handle this later, since it will require async calls that cannot be made here - } else if (componentType === "aws-resource") { + } else if (componentType === 'aws-resource') { populateResourceState(awsDeploymentEnvironment as AWSResourceDeploymentEnvironment); - if (entity.metadata["resourceType"] === "aws-rds") { - (awsDeploymentEnvironment as AWSResourceDeploymentEnvironment).resource.resourceType = "database"; + if (entity.metadata['resourceType'] === 'aws-rds') { + (awsDeploymentEnvironment as AWSResourceDeploymentEnvironment).resource.resourceType = 'database'; } } return { ...acc, - [envEntity.envName.toLowerCase()]: awsDeploymentEnvironment + [envEntity.envName.toLowerCase()]: awsDeploymentEnvironment, }; }, {}); // now build an AWS component matching the app and the deployed environment function getComponentType(entity: Entity): AWSComponentType { - if (entity.kind==="AWSEnvironment") { - return AWSComponentType.AWSEnvironment - } else if (entity.kind==="AWSEnvironmentProvider") { - return AWSComponentType.AWSProvider + if (entity.kind === 'AWSEnvironment') { + return AWSComponentType.AWSEnvironment; + } else if (entity.kind === 'AWSEnvironmentProvider') { + return AWSComponentType.AWSProvider; } - let componentType: string = entity.spec?.type?.toString() || "" + let componentType: string = entity.spec?.type?.toString() || ''; if (componentType === 'aws-resource') { - return AWSComponentType.AWSResource + return AWSComponentType.AWSResource; } else if (componentType === 'aws-app') { - return AWSComponentType.AWSApp + return AWSComponentType.AWSApp; } else if (componentType === 'aws-organization') { - return AWSComponentType.AWSOrganization + return AWSComponentType.AWSOrganization; } else { - return AWSComponentType.Default + return AWSComponentType.Default; } } function getLowerEnvironment(envs: AwsDeploymentEnvironments): GenericAWSEnvironment { let lowest: GenericAWSEnvironment = Object.values(envs).at(0)!; Object.values(envs).forEach(env => { - if (lowest && lowest.environment.level > env.environment.level) - lowest = env; - }) + if (lowest && lowest.environment.level > env.environment.level) lowest = env; + }); return lowest; } - const getRepoInfoImpl = () : IRepositoryInfo => { + const getRepoInfoImpl = (): IRepositoryInfo => { return getRepoInfo(entity); - } - - const getComponentStateType = () : ComponentStateType => { - if (entity.metadata['componentState'] ===undefined) - { - throw Error ("Error: Entity of type Component must have componentState in metadata.") + }; + + const getComponentStateType = (): ComponentStateType => { + if (entity.metadata['componentState'] === undefined) { + throw Error('Error: Entity of type Component must have componentState in metadata.'); } - const componentState = entity.metadata['componentState']?.toString() || ""; - switch (componentState) - { - case "cloudformation": - return ComponentStateType.CLOUDFORMATION - case "terraform-cloud": - return ComponentStateType.TERRAFORM_CLOUD - case "terraform-aws": + const componentState = entity.metadata['componentState']?.toString() || ''; + switch (componentState) { + case 'cloudformation': + return ComponentStateType.CLOUDFORMATION; + case 'terraform-cloud': + return ComponentStateType.TERRAFORM_CLOUD; + case 'terraform-aws': return ComponentStateType.TERRAFORM_AWS; - default: - throw Error (`Unsupported component state ${componentState}`); + default: + throw Error(`Unsupported component state ${componentState}`); } - } + }; const awsComponent: AWSComponent = { componentName: entity.metadata['name'], componentType, componentState: getComponentStateType(), - componentSubType: entity.spec? entity.spec['subType']!.toString(): "", - iacType: entity.metadata['iacType']?.toString() || "", - repoSecretArn: entity.metadata['repoSecretArn']?.toString() || "", - getRepoInfo:getRepoInfoImpl, + componentSubType: entity.spec ? entity.spec['subType']!.toString() : '', + iacType: entity.metadata['iacType']?.toString() || '', + repoSecretArn: entity.metadata['repoSecretArn']?.toString() || '', + getRepoInfo: getRepoInfoImpl, platformRegion: config.getString('backend.platformRegion'), environments: deployEnvs, - currentEnvironment: !!_change_to_env_name ? deployEnvs[_change_to_env_name.toLowerCase()] : getLowerEnvironment(deployEnvs), + currentEnvironment: !!_change_to_env_name + ? deployEnvs[_change_to_env_name.toLowerCase()] + : getLowerEnvironment(deployEnvs), setCurrentProvider: (envName: string, providerName: string) => { _setCurrentProvider(envName, providerName); - } + }, }; if (!awsComponent.currentEnvironment) { @@ -461,13 +477,19 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { awsAccount: awsComponent.currentEnvironment.providerData.accountNumber, awsRegion: awsComponent.currentEnvironment.providerData.region, prefix: awsComponent.currentEnvironment.providerData.prefix, - providerName: awsComponent.currentEnvironment.providerData.name + providerName: awsComponent.currentEnvironment.providerData.name, }); - if (awsComponent.currentEnvironment.providerData.providerType === ProviderType.SERVERLESS && componentType === "aws-app") { + if ( + awsComponent.currentEnvironment.providerData.providerType === ProviderType.SERVERLESS && + componentType === 'aws-app' + ) { await populateServerlessState(awsComponent.currentEnvironment as AWSServerlessAppDeploymentEnvironment); } - if (awsComponent.currentEnvironment.providerData.providerType === ProviderType.GENAI_SERVERLESS && componentType === "aws-app") { + if ( + awsComponent.currentEnvironment.providerData.providerType === ProviderType.GENAI_SERVERLESS && + componentType === 'aws-app' + ) { // consider populating different GenAI-related data here await populateServerlessState(awsComponent.currentEnvironment as AWSServerlessAppDeploymentEnvironment); } @@ -476,15 +498,7 @@ export const useAwsComponentFromContext = (): AwsComponentHookLoadingStatus => { } // Execute the App deployment data retrieval - const { - value: component, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => getData(), - [catalogApi, entity], - ); + const { value: component, error, loading, retry: refresh } = useAsyncRetry(() => getData(), [catalogApi, entity]); _refresh = refresh; @@ -515,11 +529,7 @@ export interface AsyncAwsAppProviderProps { export const AsyncAwsAppProvider = (props: AsyncAwsAppProviderProps) => { const { children, component, loading, error, refresh } = props; const value = { component, loading, error, refresh }; - return ( - - {children} - - ); + return {children}; }; /** @@ -548,7 +558,7 @@ export const AwsAppProvider = (props: AwsAppProviderProps) => ( ); /** - * Grab the current app's AWS deployment data, provides loading state + * Grab the current app's AWS deployment data, provides loading state * and errors, and the ability to refresh. * * @public diff --git a/backstage-plugins/plugins/aws-apps/src/hooks/useCancellablePromise.ts b/backstage-plugins/plugins/aws-apps/src/hooks/useCancellablePromise.ts index e6dda059..b88ad7f2 100644 --- a/backstage-plugins/plugins/aws-apps/src/hooks/useCancellablePromise.ts +++ b/backstage-plugins/plugins/aws-apps/src/hooks/useCancellablePromise.ts @@ -1,19 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useRef, useEffect } from "react"; +import { useRef, useEffect } from 'react'; type CancellablePromise = { promise: Promise; cancel: VoidFunction; -} +}; /** * Wraps a promise so that it is cancellable. Rejects promise when cancellation * occurs. - * + * * Example use: - * try { // invoke code that returns a cancellable promise } + * try { // invoke code that returns a cancellable promise } * catch (e) { * if ((e as any).isCanceled) { * // handle cancellation @@ -33,15 +33,15 @@ export function makeCancellableWithErrors(promise: Promise): { const wrappedPromise = new Promise((resolve, reject) => { promise - .then((val) => (isCanceled ? reject({ isCanceled }) : resolve(val))) - .catch((error) => (isCanceled ? reject({ isCanceled }) : reject(error))); + .then(val => (isCanceled ? reject({ isCanceled }) : resolve(val))) + .catch(error => (isCanceled ? reject({ isCanceled }) : reject(error))); }); return { promise: wrappedPromise, cancel() { isCanceled = true; - } + }, }; } @@ -57,13 +57,10 @@ export function makeCancellable(promise: Promise): { cancel(): void; } { let isCanceled = false; - const wrappedPromise = - new Promise((resolve, reject) => { - // Suppress resolution and rejection if canceled - promise - .then((val) => (!isCanceled && resolve(val))) - .catch((error) => (!isCanceled && reject(error))); - }); + const wrappedPromise = new Promise((resolve, reject) => { + // Suppress resolution and rejection if canceled + promise.then(val => !isCanceled && resolve(val)).catch(error => !isCanceled && reject(error)); + }); return { promise: wrappedPromise, cancel() { @@ -74,12 +71,12 @@ export function makeCancellable(promise: Promise): { /** * Returns a function that wraps a promise so that it is canceled automatically - * when the component unmounts. - * + * when the component unmounts. + * * @param options - Optional. Allows control of whether promise is rejected when * cancellation occurs. Defaults to false. * @returns a function that wraps a promise so that it is canceled automatically - * when the component unmounts. + * when the component unmounts. */ export function useCancellablePromise({ rejectOnCancel = false }): { cancellablePromise: (p: Promise) => Promise; @@ -89,9 +86,7 @@ export function useCancellablePromise({ rejectOnCancel = false }): { // test if the input argument is a cancelable promise generator if (cancellable(emptyPromise).cancel === undefined) { - throw new Error( - "promise wrapper argument must provide a cancel() function" - ); + throw new Error('promise wrapper argument must provide a cancel() function'); } const promises = useRef(); diff --git a/backstage-plugins/plugins/aws-apps/src/index.ts b/backstage-plugins/plugins/aws-apps/src/index.ts index 981245bd..0d468eb2 100644 --- a/backstage-plugins/plugins/aws-apps/src/index.ts +++ b/backstage-plugins/plugins/aws-apps/src/index.ts @@ -26,5 +26,5 @@ export { EntityAppConfigCard, EntityAuditTable, EntityEnvironmentSelector, - EntityAwsEnvironmentProviderSelectorCard + EntityAwsEnvironmentProviderSelectorCard, } from './plugin'; diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsComponentPage/AwsComponentPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsComponentPage/AwsComponentPage.tsx index cf53b7f4..54ee093f 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsComponentPage/AwsComponentPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsComponentPage/AwsComponentPage.tsx @@ -2,14 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { AsyncAwsAppProvider, useAwsComponentFromContext } from '../../hooks/useAwsApp' -import { AwsAppPage } from '../AwsAppPage/AwsAppPage' -import { AwsResourcePage } from '../AwsResourcePage/AwsResourcePage' -import { AwsPendingPage } from '../AwsPendingPage/AwsPendingPage' +import { AsyncAwsAppProvider, useAwsComponentFromContext } from '../../hooks/useAwsApp'; +import { AwsAppPage } from '../AwsAppPage/AwsAppPage'; +import { AwsResourcePage } from '../AwsResourcePage/AwsResourcePage'; +import { AwsPendingPage } from '../AwsPendingPage/AwsPendingPage'; import { EntityEnvironmentSelector } from '../../plugin'; -import { - useEntity, -} from '@backstage/plugin-catalog-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; export interface AwsComponentPageProps { componentType: string; @@ -19,14 +17,14 @@ export interface AwsComponentPageProps { export function AwsComponentPage({ componentType }: AwsComponentPageProps) { const { entity } = useEntity(); - const isApp = componentType === "aws-app"; - const isResource = componentType === "aws-resource"; - const isComponentReady = entity.metadata["appData"] !== undefined; + const isApp = componentType === 'aws-app'; + const isResource = componentType === 'aws-resource'; + const isComponentReady = entity.metadata['appData'] !== undefined; return ( { - //Before loading page - check if context exist - or AWS provisioning has not yet complete. + //Before loading page - check if context exist - or AWS provisioning has not yet complete. //if it's not ready - load an alternate pending page which only has general info and repo information isComponentReady ? ( isApp ? ( @@ -37,13 +35,13 @@ export function AwsComponentPage({ componentType }: AwsComponentPageProps) { - ) : + ) : (
No AWS matching page to render: {componentType}
- ) : - ( - ) + ) : ( + + ) }
- ) + ); } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsECSAppPage/AwsECSAppPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsECSAppPage/AwsECSAppPage.tsx index f5fd69e9..7b6f6d4b 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsECSAppPage/AwsECSAppPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsECSAppPage/AwsECSAppPage.tsx @@ -4,14 +4,16 @@ import React from 'react'; import { Grid } from '@material-ui/core'; import { EntityAboutCard } from '@backstage/plugin-catalog'; -import { EntityGeneralInfoCard, EntityAppStateCard, EntityInfrastructureInfoCard, EntityAppConfigCard, EntityAppLinksCard } from '../../plugin'; import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; + EntityGeneralInfoCard, + EntityAppStateCard, + EntityInfrastructureInfoCard, + EntityAppConfigCard, + EntityAppLinksCard, +} from '../../plugin'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -interface AwsECSAppPageProps { - -} +interface AwsECSAppPageProps {} /** @public */ export function AwsECSAppPage(_props: AwsECSAppPageProps) { @@ -42,10 +44,5 @@ export function AwsECSAppPage(_props: AwsECSAppPageProps) {
); - - return ( - <> - {awsEcsAppViewContent} - - ); + return <>{awsEcsAppViewContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsECSEnvironmentProviderPage/AwsECSEnvironmentProviderPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsECSEnvironmentProviderPage/AwsECSEnvironmentProviderPage.tsx index 31f8e89f..61684815 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsECSEnvironmentProviderPage/AwsECSEnvironmentProviderPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsECSEnvironmentProviderPage/AwsECSEnvironmentProviderPage.tsx @@ -35,12 +35,11 @@ export function AwsECSEnvironmentProviderPage(/* {children}: AwsEnvironmentProvi
- + - @@ -54,9 +53,9 @@ export function AwsECSEnvironmentProviderPage(/* {children}: AwsEnvironmentProvi - - - + + + ); diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSAppPage/AwsEKSAppPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSAppPage/AwsEKSAppPage.tsx index a17b70e3..148c278d 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSAppPage/AwsEKSAppPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSAppPage/AwsEKSAppPage.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { Grid } from '@material-ui/core'; import { EntityAboutCard } from '@backstage/plugin-catalog'; -import { EntityGeneralInfoCard, EntityAppLinksCard, EntityInfrastructureInfoCard, EntityK8sAppStateCard } from '../../plugin'; import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; + EntityGeneralInfoCard, + EntityAppLinksCard, + EntityInfrastructureInfoCard, + EntityK8sAppStateCard, +} from '../../plugin'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -interface AwsEKSAppPageProps { - -} +interface AwsEKSAppPageProps {} /** @public */ export function AwsEKSAppPage(_props: AwsEKSAppPageProps) { @@ -35,9 +36,5 @@ export function AwsEKSAppPage(_props: AwsEKSAppPageProps) { ); - return ( - <> - {awsEKSAppViewContent} - - ); + return <>{awsEKSAppViewContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSEnvironmentProviderPage/AwsEKSEnvironmentProviderPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSEnvironmentProviderPage/AwsEKSEnvironmentProviderPage.tsx index 470a1791..ac77f698 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSEnvironmentProviderPage/AwsEKSEnvironmentProviderPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsEKSEnvironmentProviderPage/AwsEKSEnvironmentProviderPage.tsx @@ -32,7 +32,7 @@ export function AwsEKSEnvironmentProviderPage(/* {children}: AwsEnvironmentProvi - + @@ -50,9 +50,9 @@ export function AwsEKSEnvironmentProviderPage(/* {children}: AwsEnvironmentProvi - - - + + + ); diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentPage/AwsEnvironmentPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentPage/AwsEnvironmentPage.tsx index 24f643fe..32a1e7eb 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentPage/AwsEnvironmentPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentPage/AwsEnvironmentPage.tsx @@ -4,21 +4,22 @@ import { EntityAboutCard, EntityLinksCard, EntityLayout } from '@backstage/plugin-catalog'; import { Grid } from '@material-ui/core'; import React, { ReactNode } from 'react'; -import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; export interface AwsEnvironmentPageProps { - children?: ReactNode + children?: ReactNode; } import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; -import { EntityAwsEnvironmentProviderSelectorCard, EntityDeleteEnvironmentCard, EntityEnvironmentInfoCard } from '../../plugin'; +import { + EntityAwsEnvironmentProviderSelectorCard, + EntityDeleteEnvironmentCard, + EntityEnvironmentInfoCard, +} from '../../plugin'; /** @public */ export function AwsEnvironmentPage(/*{children}: AwsEnvironmentPageProps */) { - const managementContent = ( diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage.tsx index d84a8d97..977bea2d 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage.tsx @@ -11,14 +11,14 @@ import { ProviderType } from '../../helpers/constants'; import { AwsEKSEnvironmentProviderPage } from '../AwsEKSEnvironmentProviderPage/AwsEKSEnvironmentProviderPage'; export interface AwsEnvironmentProviderPageProps { - children?: ReactNode + children?: ReactNode; } export function isProviderType(providerType: string, entity: Entity): (entity: Entity) => boolean { return (): boolean => { - return entity.metadata["envType"]?.toString().toLowerCase() === providerType; + return entity.metadata['envType']?.toString().toLowerCase() === providerType; }; -}; +} /** @public */ export function AwsEnvironmentProviderPage(/* {children}: AwsEnvironmentProviderPageProps */) { @@ -39,7 +39,7 @@ export function AwsEnvironmentProviderPage(/* {children}: AwsEnvironmentProvider -

Environment Provider Type "{entity.metadata["envType"]?.toString()}" Is Not Supported At This Time

+

Environment Provider Type "{entity.metadata['envType']?.toString()}" Is Not Supported At This Time

); diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsRDSResourcePage/AwsRDSResourcePage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsRDSResourcePage/AwsRDSResourcePage.tsx index 64443fe3..d14afe92 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsRDSResourcePage/AwsRDSResourcePage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsRDSResourcePage/AwsRDSResourcePage.tsx @@ -4,13 +4,11 @@ import { Grid } from '@material-ui/core'; import React, { ReactNode } from 'react'; import { EntityAboutCard, EntityLinksCard } from '@backstage/plugin-catalog'; -import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; import { EntityInfrastructureInfoCard } from '../../plugin'; interface AwsRDSResourcePageProps { - children?: ReactNode + children?: ReactNode; } /** @public */ @@ -31,9 +29,5 @@ export function AwsRDSResourcePage(_props: AwsRDSResourcePageProps) {
); - return ( - <> - {rdsContent} - - ); + return <>{rdsContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsResourcePage/AwsResourcePage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsResourcePage/AwsResourcePage.tsx index 7c1ba573..12ad45cf 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsResourcePage/AwsResourcePage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsResourcePage/AwsResourcePage.tsx @@ -10,8 +10,8 @@ import React, { ReactNode } from 'react'; import { CICDContent } from '../../components/CICDContent/CICDContent'; import { EntityDeleteAppCard } from '../../plugin'; import { AwsRDSResourcePage } from '../AwsRDSResourcePage/AwsRDSResourcePage'; -import {AwsS3ResourcePage} from '../AwsS3ResourcePage/AwsS3ResourcePage' -import {AwsSecretsManagerResourcePage} from '../AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage' +import { AwsS3ResourcePage } from '../AwsS3ResourcePage/AwsS3ResourcePage'; +import { AwsSecretsManagerResourcePage } from '../AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage'; interface AwsResourcePageProps { children: ReactNode; @@ -78,7 +78,7 @@ export function AwsResourcePage(_props: AwsResourcePageProps) { {_props.children} - + diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsS3ResourcePage/AwsS3ResourcePage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsS3ResourcePage/AwsS3ResourcePage.tsx index 58362488..cbc010ce 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsS3ResourcePage/AwsS3ResourcePage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsS3ResourcePage/AwsS3ResourcePage.tsx @@ -4,13 +4,11 @@ import { Grid } from '@material-ui/core'; import React, { ReactNode } from 'react'; import { EntityAboutCard, EntityLinksCard } from '@backstage/plugin-catalog'; -import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; import { EntityInfrastructureInfoCard } from '../../plugin'; interface AwsS3ResourcePageProps { - children?: ReactNode + children?: ReactNode; } /** @public */ @@ -31,9 +29,5 @@ export function AwsS3ResourcePage(_props: AwsS3ResourcePageProps) {
); - return ( - <> - {rdsContent} - - ); + return <>{rdsContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage.tsx index 6bf43c38..b5d38e11 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsSecretsManagerResourcePage/AwsSecretsManagerResourcePage.tsx @@ -4,13 +4,11 @@ import { Grid } from '@material-ui/core'; import React, { ReactNode } from 'react'; import { EntityAboutCard, EntityLinksCard } from '@backstage/plugin-catalog'; -import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; import { EntityInfrastructureInfoCard } from '../../plugin'; interface AwsSecretsManagerResourcePageProps { - children?: ReactNode + children?: ReactNode; } /** @public */ @@ -31,9 +29,5 @@ export function AwsSecretsManagerResourcePage(_props: AwsSecretsManagerResourceP ); - return ( - <> - {rdsContent} - - ); + return <>{rdsContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessAppPage/AwsServerlessAppPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessAppPage/AwsServerlessAppPage.tsx index e74eb93d..62984f60 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessAppPage/AwsServerlessAppPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessAppPage/AwsServerlessAppPage.tsx @@ -3,19 +3,19 @@ import React from 'react'; import { EntityAboutCard } from '@backstage/plugin-catalog'; -import { EntityAppLinksCard, EntityAppStateCardCloudFormation, EntityGeneralInfoCard, EntityInfrastructureInfoCard } from '../../plugin'; -import { Grid } from '@material-ui/core'; import { - EntityCatalogGraphCard -} from '@backstage/plugin-catalog-graph'; - -interface AwsServerlessAppPageProps { + EntityAppLinksCard, + EntityAppStateCardCloudFormation, + EntityGeneralInfoCard, + EntityInfrastructureInfoCard, +} from '../../plugin'; +import { Grid } from '@material-ui/core'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; -} +interface AwsServerlessAppPageProps {} /** @public */ export function AwsServerlessAppPage(_props: AwsServerlessAppPageProps) { - const awsServerlessRestApiAppViewContent = ( @@ -39,9 +39,5 @@ export function AwsServerlessAppPage(_props: AwsServerlessAppPageProps) { ); - return ( - <> - {awsServerlessRestApiAppViewContent} - - ); + return <>{awsServerlessRestApiAppViewContent}; } diff --git a/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessEnvironmentProviderPage/AwsServerlessEnvironmentProviderPage.tsx b/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessEnvironmentProviderPage/AwsServerlessEnvironmentProviderPage.tsx index 8f54db02..bb1e1352 100644 --- a/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessEnvironmentProviderPage/AwsServerlessEnvironmentProviderPage.tsx +++ b/backstage-plugins/plugins/aws-apps/src/pages/AwsServerlessEnvironmentProviderPage/AwsServerlessEnvironmentProviderPage.tsx @@ -35,7 +35,7 @@ export function AwsServerlessEnvironmentProviderPage(/* {children}: AwsEnvironme - + @@ -53,9 +53,9 @@ export function AwsServerlessEnvironmentProviderPage(/* {children}: AwsEnvironme - - - + + + ); diff --git a/backstage-plugins/plugins/aws-apps/src/plugin.ts b/backstage-plugins/plugins/aws-apps/src/plugin.ts index b1f00d8c..3a2e17ac 100644 --- a/backstage-plugins/plugins/aws-apps/src/plugin.ts +++ b/backstage-plugins/plugins/aws-apps/src/plugin.ts @@ -88,7 +88,8 @@ export const EntityAppStateCardCloudFormation = opaPlugin.provide( createComponentExtension({ name: 'AppStateCardCloudFormation', component: { - lazy: () => import('./components/AppStateCardCloudFormation/AppStateCardCloudFormation').then(m => m.AppStateCard), + lazy: () => + import('./components/AppStateCardCloudFormation/AppStateCardCloudFormation').then(m => m.AppStateCard), }, }), ); @@ -147,7 +148,6 @@ export const EntityInfrastructureInfoCard = opaPlugin.provide( }), ); - export const EntityProviderInfoCard = opaPlugin.provide( createComponentExtension({ name: 'ProviderInfoCard', @@ -215,7 +215,10 @@ export const EntityAwsEnvironmentProviderSelectorCard = opaPlugin.provide( createComponentExtension({ name: 'AwsEnvironmentProviderSelectorCard', component: { - lazy: () => import('./components/AwsEnvironmentProviderCard/AwsEnvironmentProviderCard').then(m => m.AwsEnvironmentProviderCardWidget), + lazy: () => + import('./components/AwsEnvironmentProviderCard/AwsEnvironmentProviderCard').then( + m => m.AwsEnvironmentProviderCardWidget, + ), }, }), ); @@ -251,7 +254,8 @@ export const AwsEnvironmentProviderPage = opaPlugin.provide( createComponentExtension({ name: 'AwsEnvironmentProviderPage', component: { - lazy: () => import('./pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage').then(m => m.AwsEnvironmentProviderPage), + lazy: () => + import('./pages/AwsEnvironmentProviderPage/AwsEnvironmentProviderPage').then(m => m.AwsEnvironmentProviderPage), }, }), ); diff --git a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/package.json b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/package.json index 328b7b38..6213469e 100644 --- a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/package.json +++ b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/package.json @@ -25,7 +25,7 @@ "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", - "pluginPackage": "@backstage/plugin-catalog-node" + "pluginPackage": "@backstage/plugin-catalog-backend" }, "scripts": { "start": "backstage-cli package start", diff --git a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/index.ts b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/index.ts index c9dd301b..81f59679 100644 --- a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/index.ts +++ b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/index.ts @@ -7,8 +7,6 @@ export { catalogModuleAwsAppsEntitiesProcessor as default } from './module'; - export { AWSEnvironmentEntitiesProcessor } from './processor/AWSEnvironmentEntitiesProcessor'; export { AWSEnvironmentProviderEntitiesProcessor } from './processor/AWSEnvironmentProviderEntitiesProcessor'; - diff --git a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/module.ts b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/module.ts index f2ac2f04..0c28ac26 100644 --- a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/module.ts +++ b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/module.ts @@ -1,20 +1,17 @@ -import { - coreServices, - createBackendModule, -} from '@backstage/backend-plugin-api'; +import { coreServices, createBackendModule } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import {AWSEnvironmentEntitiesProcessor} from './processor/AWSEnvironmentEntitiesProcessor' -import { AWSEnvironmentProviderEntitiesProcessor } from './processor/AWSEnvironmentProviderEntitiesProcessor' +import { AWSEnvironmentEntitiesProcessor } from './processor/AWSEnvironmentEntitiesProcessor'; +import { AWSEnvironmentProviderEntitiesProcessor } from './processor/AWSEnvironmentProviderEntitiesProcessor'; export const catalogModuleAwsAppsEntitiesProcessor = createBackendModule({ pluginId: 'catalog', moduleId: 'aws-apps-entities-processor', register(reg) { reg.registerInit({ - deps: { + deps: { logger: coreServices.logger, catalog: catalogProcessingExtensionPoint, - }, + }, async init({ catalog, logger }) { logger.info('Hello World from your AWS custom entities processor!'); catalog.addProcessor(new AWSEnvironmentEntitiesProcessor()); @@ -25,4 +22,3 @@ export const catalogModuleAwsAppsEntitiesProcessor = createBackendModule({ }); export default catalogModuleAwsAppsEntitiesProcessor; - diff --git a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentEntitiesProcessor.ts b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentEntitiesProcessor.ts index 63a6465b..fec07596 100644 --- a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentEntitiesProcessor.ts +++ b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentEntitiesProcessor.ts @@ -2,120 +2,105 @@ // SPDX-License-Identifier: Apache-2.0 import { - Entity, - getCompoundEntityRef, - parseEntityRef, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - RELATION_DEPENDS_ON, - RELATION_DEPENDENCY_OF, - } from '@backstage/catalog-model'; - import { - CatalogProcessor, - CatalogProcessorEmit, - LocationSpec, - processingResult, - } from '@backstage/plugin-catalog-node'; - import { - AWSEnvironmentEntityV1, - awsEnvironmentEntityV1Validator, - } from '@aws/plugin-aws-apps-common-for-backstage'; - - /** @public */ - export class AWSEnvironmentEntitiesProcessor implements CatalogProcessor { - getProcessorName(): string { - return 'AWSEnvironmentEntitiesProcessor'; + Entity, + getCompoundEntityRef, + parseEntityRef, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_DEPENDS_ON, + RELATION_DEPENDENCY_OF, +} from '@backstage/catalog-model'; +import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, processingResult } from '@backstage/plugin-catalog-node'; +import { AWSEnvironmentEntityV1, awsEnvironmentEntityV1Validator } from '@aws/plugin-aws-apps-common-for-backstage'; + +/** @public */ +export class AWSEnvironmentEntitiesProcessor implements CatalogProcessor { + getProcessorName(): string { + return 'AWSEnvironmentEntitiesProcessor'; + } + + private readonly validators = [awsEnvironmentEntityV1Validator]; + + async validateEntityKind(entity: Entity): Promise { + for (const validator of this.validators) { + if (entity.kind === 'AWSEnvironment') + if (await validator.check(entity)) { + return true; + } } - - private readonly validators = [awsEnvironmentEntityV1Validator]; - - async validateEntityKind(entity: Entity): Promise { - for (const validator of this.validators) { - if (entity.kind === "AWSEnvironment") - if (await validator.check(entity)) { - return true; - } + + return false; + } + + async postProcessEntity(entity: Entity, _location: LocationSpec, emit: CatalogProcessorEmit): Promise { + const selfRef = getCompoundEntityRef(entity); + + if (entity.apiVersion === 'aws.backstage.io/v1alpha' && entity.kind === 'AWSEnvironment') { + const template = entity as AWSEnvironmentEntityV1; + + const target = template.spec.owner; + if (target) { + const targetRef = parseEntityRef(target, { + defaultKind: 'Group', + defaultNamespace: selfRef.namespace, + }); + emit( + processingResult.relation({ + source: selfRef, + type: RELATION_OWNED_BY, + target: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + }), + ); + emit( + processingResult.relation({ + source: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + type: RELATION_OWNER_OF, + target: selfRef, + }), + ); } - - return false; - } - - async postProcessEntity( - entity: Entity, - _location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise { - const selfRef = getCompoundEntityRef(entity); - - if ( - entity.apiVersion === 'aws.backstage.io/v1alpha' && - entity.kind === 'AWSEnvironment' - ) { - const template = entity as AWSEnvironmentEntityV1; - - const target = template.spec.owner; - if (target) { - const targetRef = parseEntityRef(target, { - defaultKind: 'Group', + if (template.spec.dependsOn) { + template.spec.dependsOn.forEach(awsEnv => { + const targetRef = parseEntityRef(awsEnv, { + defaultKind: 'awsenvironmentprovider', defaultNamespace: selfRef.namespace, }); - emit( - processingResult.relation({ - source: selfRef, - type: RELATION_OWNED_BY, - target: { - kind: targetRef.kind, - namespace: targetRef.namespace, - name: targetRef.name, - }, - }), - ); - emit( - processingResult.relation({ - source: { - kind: targetRef.kind, - namespace: targetRef.namespace, - name: targetRef.name, - }, - type: RELATION_OWNER_OF, - target: selfRef, - }), - ); - } - if (template.spec.dependsOn) { - template.spec.dependsOn.forEach(awsEnv => { - const targetRef = parseEntityRef(awsEnv, { - defaultKind: 'awsenvironmentprovider', - defaultNamespace: selfRef.namespace, - }); - if (targetRef.kind == 'awsenvironmentprovider') { - emit( - processingResult.relation({ - source: selfRef, - type: RELATION_DEPENDS_ON, - target: { - kind: targetRef.kind, - namespace: targetRef.namespace, - name: targetRef.name, - }, - }), - ); - emit( - processingResult.relation({ - source: { - kind: targetRef.kind, - namespace: targetRef.namespace, - name: targetRef.name, - }, - type: RELATION_DEPENDENCY_OF, - target: selfRef, - }), - ); - } - }); - } + if (targetRef.kind == 'awsenvironmentprovider') { + emit( + processingResult.relation({ + source: selfRef, + type: RELATION_DEPENDS_ON, + target: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + }), + ); + emit( + processingResult.relation({ + source: { + kind: targetRef.kind, + namespace: targetRef.namespace, + name: targetRef.name, + }, + type: RELATION_DEPENDENCY_OF, + target: selfRef, + }), + ); + } + }); } - - return entity; } - } \ No newline at end of file + + return entity; + } +} diff --git a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentProviderEntitiesProcessor.ts b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentProviderEntitiesProcessor.ts index fda782e0..d1d44d89 100644 --- a/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentProviderEntitiesProcessor.ts +++ b/backstage-plugins/plugins/catalog-backend-module-aws-apps-entities-processor/src/processor/AWSEnvironmentProviderEntitiesProcessor.ts @@ -10,8 +10,10 @@ import { } from '@backstage/catalog-model'; import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { AWSEnvironmentProviderEntityV1, awsEnvironmentProviderEntityV1Validator } from '@aws/plugin-aws-apps-common-for-backstage'; - +import { + AWSEnvironmentProviderEntityV1, + awsEnvironmentProviderEntityV1Validator, +} from '@aws/plugin-aws-apps-common-for-backstage'; /** @public */ export class AWSEnvironmentProviderEntitiesProcessor implements CatalogProcessor { diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/README.md b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/README.md index 88727e7b..4551b048 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/README.md +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/README.md @@ -1,6 +1,6 @@ - # AWS Apps Scaffolder Actions @@ -49,6 +49,7 @@ const actions = [ ]; ... ``` + After the scaffolder configuration is updated, you can use the new actions in your Software Templates. ## AWS Apps Scaffolder Actions @@ -61,7 +62,7 @@ Reference the plugin documentation to understand how to create and surface AWS E ### Get Environment Providers -The `opa:get-env-providers` scaffolder action retreives AWS environment providers so that their configurations can be used by other template actions. Refer to the `/create/actions` path of your Backstage instance for details on the returned output. +The `opa:get-env-providers` scaffolder action retreives AWS environment providers so that their configurations can be used by other template actions. Refer to the `/create/actions` path of your Backstage instance for details on the returned output. ```yaml # template.yaml @@ -80,9 +81,9 @@ The `opa:get-env-providers` scaffolder action retreives AWS environment provider ### Create AWS SecretsManager Secrets -The `opa:create-secret` scaffolder action creates a new Secret in the [AWS Secrets Manager service](https://aws.amazon.com/secrets-manager/). +The `opa:create-secret` scaffolder action creates a new Secret in the [AWS Secrets Manager service](https://aws.amazon.com/secrets-manager/). -The template snippet below demonstrates this action in the `steps` section of a Backstage Software Template. See the example in the plugin's [src/example/template.yaml][example_template] file to better understand the action in the context of a full template. +The template snippet below demonstrates this action in the `steps` section of a Backstage Software Template. See the example in the plugin's [src/example/template.yaml][example_template] file to better understand the action in the context of a full template. This action will generate a `awsSecretArn` output which can be referenced in subsequent scaffolder steps. @@ -114,9 +115,9 @@ This action will generate a `awsSecretArn` output which can be referenced in sub ### Create Gitlab Repo Access Token -The `opa:createRepoAccessToken:gitlab` scaffolder action creates a [project access token][gitlab_pat] where access is restrited to a specific Gitlab repository. +The `opa:createRepoAccessToken:gitlab` scaffolder action creates a [project access token][gitlab_pat] where access is restrited to a specific Gitlab repository. -The template snippet below demonstrates this action in the `steps` section of a Backstage Software Template. See the example in the plugin's [src/example/template.yaml][example_template] file to better understand the action in the context of a full template. +The template snippet below demonstrates this action in the `steps` section of a Backstage Software Template. See the example in the plugin's [src/example/template.yaml][example_template] file to better understand the action in the context of a full template. ```yaml # template.yaml @@ -134,7 +135,7 @@ The template snippet below demonstrates this action in the `steps` section of a repoUrl: ${{ parameters.repoUrl }} # the project id of the gitlab repo projectId: ${{ steps['publish'].output.projectId }} - # the ARN of the Secrets Manager Secret where the access token should be + # the ARN of the Secrets Manager Secret where the access token should be # securely stored secretArn: ${{ steps['createSecretManager'].output.awsSecretArn }} ... @@ -143,9 +144,9 @@ The template snippet below demonstrates this action in the `steps` section of a ### Get Platform Metadata -The `opa:get-platform-metadata` scaffolder action retrieves information about the platform and environment on which OPA on AWS is running. +The `opa:get-platform-metadata` scaffolder action retrieves information about the platform and environment on which OPA on AWS is running. -The action will return the AWS region where the platform is running. Future metadata is also planned. +The action will return the AWS region where the platform is running. Future metadata is also planned. ```yaml # template.yaml @@ -184,7 +185,9 @@ The action will return a `params` response as an object containing a map of SSM ... ``` + The returned object for the example above: + ```json { "/my/ssm/parameter1": "Parameter 1's value", @@ -194,7 +197,7 @@ The returned object for the example above: ### Get SSM Parameters -The `opa:get-ssm-parameters` scaffolder action is very similar to the action above except that it will retrieve AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions. This action is often used in conjunction with the `opa:get-env-providers` action's response of an array of environment providers. +The `opa:get-ssm-parameters` scaffolder action is very similar to the action above except that it will retrieve AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions. This action is often used in conjunction with the `opa:get-env-providers` action's response of an array of environment providers. The action will return a `params` response as an object containing a map of SSM parameters keyed off of the environment provider name. @@ -216,7 +219,9 @@ The action will return a `params` response as an object containing a map of SSM ... ``` + The returned object for the example above: + ```json { "env-provider-A": { @@ -226,11 +231,11 @@ The returned object for the example above: "env-provider-B": { "/B/parameter1": "Parameter 1's value in env-B", "/B/parameter2": "Parameter 2's value in env-B" - }, + } } ``` - + [gitlab_pat]: https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html 'Gitlab Project Access Tokens' [example_template]: src/example/template.yaml diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/api-report.md b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/api-report.md new file mode 100644 index 00000000..cd33d377 --- /dev/null +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/api-report.md @@ -0,0 +1,139 @@ +## API Report File for "@aws/plugin-scaffolder-backend-aws-apps-for-backstage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AwsAuthResponse } from '@aws/plugin-aws-apps-backend-for-backstage'; +import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; + +// @public (undocumented) +export function createRepoAccessTokenAction(options: { + integrations: ScmIntegrationRegistry; + envConfig: Config; +}): TemplateAction< + { + repoUrl: string; + secretArn: string; + projectId: number; + region?: string | undefined; + }, + JsonObject +>; + +// @public (undocumented) +export function createS3BucketAction(): TemplateAction< + { + bucketName: string; + envProviders: EnvironmentProvider[]; + tags?: + | { + Key: string; + Value: string | number | boolean; + }[] + | undefined; + }, + JsonObject +>; + +// @public (undocumented) +export function createSecretAction(options: { + envConfig: Config; +}): TemplateAction< + { + secretName: string; + description?: string | undefined; + region?: string | undefined; + tags?: + | { + Key: string; + Value: string | number | boolean; + }[] + | undefined; + }, + JsonObject +>; + +// @public (undocumented) +export type EnvironmentProvider = { + envProviderName: string; + envProviderType: string; + envProviderPrefix: string; + accountId: string; + region: string; + vpcId: string; + publicSubnets: string; + privateSubnets: string; + clusterArn?: string; + assumedRoleArn: string; + kubectlLambdaArn?: string; + kubectlLambdaRoleArn?: string; +}; + +// @public (undocumented) +export type EnvironmentProviderConnection = { + providerName: string; + accountId: string; + region: string; + awsAuthResponse: AwsAuthResponse; +}; + +// @public (undocumented) +export function getComponentInfoAction(): TemplateAction< + { + componentName: string; + }, + JsonObject +>; + +// @public (undocumented) +export function getEnvProvidersAction(options: { + config: Config; + logger: LoggerService; + catalogClient: CatalogApi; +}): TemplateAction< + { + environmentRef: string; + }, + JsonObject +>; + +// @public (undocumented) +export function getPlatformMetadataAction(options: { + envConfig: Config; +}): TemplateAction; + +// @public (undocumented) +export function getPlatformParametersAction(options: { + envConfig: Config; +}): TemplateAction< + { + paramKeys: string[]; + region?: string | undefined; + }, + JsonObject +>; + +// @public (undocumented) +export function getSsmParametersAction( + config: Config, + logger: LoggerService, +): TemplateAction< + { + paramKeys: string[]; + envProviders: EnvironmentProvider[]; + }, + JsonObject +>; + +// @public (undocumented) +const scaffolderModuleAwsApps: BackendFeatureCompat; +export default scaffolderModuleAwsApps; + +// (No @packageDocumentation comment for this package) +``` diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/package.json b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/package.json index 24b26029..12c7ecdb 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/package.json +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/package.json @@ -37,20 +37,20 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@aws-sdk/client-sfn": "^3.623.0", + "@aws/plugin-aws-apps-backend-for-backstage": "^0.3.4", + "@aws-sdk/client-secrets-manager": "^3.623.0", + "@aws-sdk/client-sfn": "^3.623.0", "@aws-sdk/client-ssm": "^3.623.0", "@aws-sdk/types": "^3.609.0", - "@aws-sdk/client-secrets-manager": "^3.623.0", "@aws-sdk/util-arn-parser": "^3.568.0", "@backstage/catalog-client": "^1.6.5", "@backstage/catalog-model": "^1.5.0", "@backstage/config": "^1.2.0", + "@backstage/errors": "^1.2.4", + "@backstage/integration": "^1.13.0", "@backstage/plugin-scaffolder-backend": "^1.23.0", "@backstage/plugin-scaffolder-node": "^0.4.8", - "@backstage/integration": "^1.13.0", - "@backstage/errors": "^1.2.4", "@backstage/types": "^1.1.1", - "@aws/plugin-aws-apps-backend-for-backstage": "^0.3.4", "lodash": "^4.17.21", "winston": "^3.13.1", "yaml": "^2.5.0" diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/create-repoAccesstoken.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/create-repoAccesstoken.ts index 02f5fe21..5a516fa1 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/create-repoAccesstoken.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/create-repoAccesstoken.ts @@ -7,9 +7,10 @@ import { parseRepoUrl } from '../../helpers/util'; import { InputError } from '@backstage/errors'; import { validate as validateArn } from '@aws-sdk/util-arn-parser'; import { putSecret } from '../../helpers/action-context'; -import { Config } from '@backstage/config' +import { Config } from '@backstage/config'; -export function createRepoAccessTokenAction(options: { integrations: ScmIntegrationRegistry, envConfig: Config }) { +/** @public */ +export function createRepoAccessTokenAction(options: { integrations: ScmIntegrationRegistry; envConfig: Config }) { const { integrations, envConfig } = options; return createTemplateAction<{ repoUrl: string; @@ -44,9 +45,11 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat }, }, async handler(ctx) { - let { repoUrl, projectId, secretArn, region } = ctx.input; + const { repoUrl, projectId, secretArn } = ctx.input; + let { region } = ctx.input; + if (!region) { - region = envConfig.getString('backend.platformRegion') + region = envConfig.getString('backend.platformRegion'); } const { repo, host } = parseRepoUrl(repoUrl, integrations); ctx.logger.info(`Project Id: ${projectId}`); @@ -55,7 +58,6 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat const repoToken = await createRepoToken(); await putSecret(secretArn, repoToken, region, ctx.logger); - /** * helper function to create a repo token for Gitlab * @returns token for the repo @@ -63,7 +65,7 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat async function createRepoToken(): Promise { if (!integrationConfig) { throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config` + `No matching integration configuration for host ${host}, please check your integrations config`, ); } if (!integrationConfig.config.token) { @@ -72,7 +74,7 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat const token = integrationConfig.config.token!; // get the apiBaseUrl - let apiBaseUrl = integrationConfig.config.apiBaseUrl ?? `https://${host}/api/v4`; + const apiBaseUrl = integrationConfig.config.apiBaseUrl ?? `https://${host}/api/v4`; if (!validateArn(secretArn)) { throw new Error(`Invalid ARN provided for Secret: ${secretArn}`); @@ -99,12 +101,10 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat // We have a successful response, so return the token from the response data const data = await res.json(); return data.token as string; - } else { - const message = `Failed to create repo access token: ${res.status}: ${res.statusText}` - ctx.logger.info(message); - throw new Error(message); } - + const message = `Failed to create repo access token: ${res.status}: ${res.statusText}`; + ctx.logger.info(message); + throw new Error(message); } /** @@ -112,7 +112,7 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat * so that it is (almost) the maximum date for a Gitlab personal access token. * The maximum time is 365 days, but we're being conservative to account for * locales, timezones, and leap years which may interfere with exact calculations. - * + * * Returned string will be in YYYY-MM-DD format */ function getExpiryDate(): string { @@ -120,7 +120,6 @@ export function createRepoAccessTokenAction(options: { integrations: ScmIntegrat date.setDate(date.getDate() + 364); return date.toISOString().split('T')[0]; } - }, }); } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/index.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/index.ts index 050fa056..45fab0bc 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/index.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-repo-access-token/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { createRepoAccessTokenAction } from './create-repoAccesstoken'; \ No newline at end of file +export { createRepoAccessTokenAction } from './create-repoAccesstoken'; diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-s3-bucket/create-s3-bucket.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-s3-bucket/create-s3-bucket.ts index 6ef3a524..e902854d 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-s3-bucket/create-s3-bucket.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-s3-bucket/create-s3-bucket.ts @@ -3,13 +3,14 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; // import { getAWScreds, AwsAppsApi, createAuditRecord } from '@aws/plugin-aws-apps-backend-for-backstage'; -import { EnvironmentProvider, } from '../../types'; +import { EnvironmentProvider } from '../../types'; +/** @public */ export function createS3BucketAction() { return createTemplateAction<{ bucketName: string; envProviders: EnvironmentProvider[]; - tags?: { Key: string, Value: string | number | boolean }[]; + tags?: { Key: string; Value: string | number | boolean }[]; }>({ id: 'opa:create-s3-bucket', description: 'Creates an S3 bucket', @@ -38,8 +39,8 @@ export function createS3BucketAction() { type: 'object', properties: { Key: { type: 'string' }, - Value: { type: ['string', 'number', 'boolean'] } - } + Value: { type: ['string', 'number', 'boolean'] }, + }, }, ], }, @@ -47,9 +48,7 @@ export function createS3BucketAction() { }, output: { type: 'object', - required: [ - 'awsBucketName', - ], + required: ['awsBucketName'], properties: { awsBucketName: { title: 'S3 Bucket Name', @@ -59,24 +58,16 @@ export function createS3BucketAction() { }, }, async handler() { - // We plan to remove/depricate this scaffolder action... - // const { bucketName, tags, envProviders } = ctx.input; - // // TODO add support for multiaccount/multiregion // const { accountId, region } = envProviders[0]; - // const creds = await getAWScreds(accountId, region, ctx.user!.entity!); - // const apiClient = new AwsAppsApi(ctx.logger, creds.credentials, region, accountId); - // ctx.logger.info(`Creating bucket with name: ${bucketName}-${accountId}-${region}`); - // try { // const response = await apiClient.createS3Bucket(bucketName, tags); // ctx.output('awsBucketName', response.Location!.slice(1)); - // const auditResponse = await createAuditRecord({ // actionType: 'Create S3 Bucket', // actionName: response.Location!.slice(1), @@ -86,11 +77,10 @@ export function createS3BucketAction() { // awsRegion: region, // logger: ctx.logger, // requester: ctx.user!.entity!.metadata.name, - // status: response.$metadata.httpStatusCode == 200 ? 'SUCCESS' : 'FAILED', + // status: response.$metadata.httpStatusCode === 200 ? 'SUCCESS' : 'FAILED', // owner: creds.owner || '', // envProviderName: "FIXME", // FIXME createS3BucketAction pass envProviderName // envProviderPrefix: "FIXME", // FIXME createS3BucketAction pass envProviderPrefix - // }); // if (auditResponse.status === 'FAILED') { // throw Error; diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/create-secret.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/create-secret.ts index ac83cf64..8070597b 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/create-secret.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/create-secret.ts @@ -3,15 +3,16 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { createSecret } from '../../helpers/action-context'; -import { Config } from '@backstage/config' +import { Config } from '@backstage/config'; +/** @public */ export function createSecretAction(options: { envConfig: Config }) { const { envConfig } = options; return createTemplateAction<{ secretName: string; description?: string; region?: string; - tags?: { Key: string, Value: string | number | boolean }[]; + tags?: { Key: string; Value: string | number | boolean }[]; }>({ id: 'opa:create-secret', description: 'Creates secret in Secret Manager', @@ -45,8 +46,8 @@ export function createSecretAction(options: { envConfig: Config }) { type: 'object', properties: { Key: { type: 'string' }, - Value: { type: ['string', 'number', 'boolean'] } - } + Value: { type: ['string', 'number', 'boolean'] }, + }, }, ], }, @@ -63,20 +64,20 @@ export function createSecretAction(options: { envConfig: Config }) { }, }, async handler(ctx) { - let { secretName, description, region, tags } = ctx.input; + const { secretName, description, tags } = ctx.input; + let { region } = ctx.input; + if (!region) { - region = envConfig.getString('backend.platformRegion') + region = envConfig.getString('backend.platformRegion'); } const secretDescription = description ?? 'Secret created from Backstage scaffolder action'; try { const ARN = await createSecret(secretName, secretDescription, region, tags, ctx.logger); ctx.output('awsSecretArn', ARN!); - } catch (e) { throw new Error(e instanceof Error ? e.message : JSON.stringify(e)); - }; + } }, - }); } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/index.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/index.ts index 07030657..edbcf1df 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/index.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/create-secret/index.ts @@ -1,4 +1,4 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { createSecretAction } from './create-secret'; \ No newline at end of file +export { createSecretAction } from './create-secret'; diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-component-info/get-component-info.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-component-info/get-component-info.ts index 0ca4d3a0..20081799 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-component-info/get-component-info.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-component-info/get-component-info.ts @@ -25,6 +25,7 @@ const examples = [ }, ]; +/** @public */ export function getComponentInfoAction() { return createTemplateAction<{ componentName: string; @@ -46,9 +47,7 @@ export function getComponentInfoAction() { }, output: { type: 'object', - required: [ - 'kebabCaseComponentName', - ], + required: ['kebabCaseComponentName'], properties: { kebabCaseComponentName: { title: 'The component name, converted to kebab case', @@ -58,7 +57,7 @@ export function getComponentInfoAction() { }, }, async handler(ctx) { - const { componentName, } = ctx.input; + const { componentName } = ctx.input; const kebabComponentName = kebabCase(componentName); diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/get-env-providers.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/get-env-providers.ts index fc3275dc..8a67346d 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/get-env-providers.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/get-env-providers.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { CatalogApi } from '@backstage/catalog-client'; -import { JsonArray, } from '@backstage/types'; +import { JsonArray } from '@backstage/types'; import { Entity, EntityRelation, RELATION_DEPENDS_ON } from '@backstage/catalog-model'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; @@ -10,11 +10,15 @@ import { getAWScreds } from '@aws/plugin-aws-apps-backend-for-backstage'; import { getSSMParameterValue } from '../../helpers/action-context'; import { EnvironmentProvider } from '../../types'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; + const ID = 'opa:get-env-providers'; const examples = [ { - description: 'Retreive AWS environment providers so that their configurations can be used by other template actions', + description: + 'Retrieve AWS environment providers so that their configurations can be used by other template actions', example: yaml.stringify({ steps: [ { @@ -47,8 +51,9 @@ interface DeploymentParameters { kubectlLambdaRoleArn?: string; } -export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { - const { catalogClient } = options; +/** @public */ +export function getEnvProvidersAction(options: { config: Config; logger: LoggerService; catalogClient: CatalogApi }) { + const { config, logger, catalogClient } = options; return createTemplateAction<{ environmentRef: string; @@ -70,13 +75,7 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { }, output: { type: 'object', - required: [ - 'envName', - 'envShortName', - 'envRef', - 'envDeployManualApproval', - 'envProviders', - ], + required: ['envName', 'envShortName', 'envRef', 'envDeployManualApproval', 'envProviders'], properties: { envName: { title: 'The AWS environment name', @@ -108,7 +107,7 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { 'vpcId', 'publicSubnets', 'privateSubnets', - 'assumedRoleArn' + 'assumedRoleArn', ], properties: { envProviderName: { @@ -140,7 +139,8 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { type: 'array', }, clusterArn: { - title: 'The Arn of the cluster where the service and task are deployed, if needed. A cluster could be ECS or EKS', + title: + 'The Arn of the cluster where the service and task are deployed, if needed. A cluster could be ECS or EKS', type: 'string', }, assumedRoleArn: { @@ -148,17 +148,19 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { type: 'string', }, kubectlLambdaArn: { - title: 'EKS Only - The Arn of the lambda function that that can execute kubectl commands against the provider\'s EKS cluster', + title: + "EKS Only - The Arn of the lambda function that that can execute kubectl commands against the provider's EKS cluster", type: 'string', }, kubectlLambdaRoleArn: { - title: 'The Arn of the IAM role for the lambda function that that can execute kubectl commands against the provider\'s EKS cluster', + title: + "The Arn of the IAM role for the lambda function that that can execute kubectl commands against the provider's EKS cluster", type: 'string', }, - } - } + }, + }, }, - } + }, }, }, async handler(ctx) { @@ -171,30 +173,35 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { if (ctx.user?.entity === undefined) { // Verify the automationKey value. If it matches, set an automation user in the context if (ctx.secrets?.automationKey === process.env.AUTOMATION_KEY) { - console.log("Automation key provided to use automation user"); + console.log('Automation key provided to use automation user'); ctx.user = { entity: { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: 'automation' }, - spec: { profile: { displayName: "Automation User" } } - } - } + spec: { profile: { displayName: 'Automation User' } }, + }, + }; } else { ctx.logger.info(`No user context provided for ${ID} action`); throw new Error(`No user context provided for ${ID} action`); } } - const awsEnvEntity = await catalogClient.getEntityByRef(environmentRef, { token }); + const awsEnvEntity = await catalogClient.getEntityByRef(environmentRef, { + token, + }); if (awsEnvEntity === undefined) { throw new Error(`The environment entity "${environmentRef}" could not be located in the catalog.`); } - const envShortName = awsEnvEntity.metadata['shortName']?.toString() || ''; + const envShortName = awsEnvEntity.metadata.shortName?.toString() || ''; ctx.output('envName', awsEnvEntity.metadata.name); ctx.output('envRef', environmentRef); - ctx.output('envDeployManualApproval', "true" === awsEnvEntity.metadata['deploymentRequiresApproval']?.toString() || '') + ctx.output( + 'envDeployManualApproval', + awsEnvEntity.metadata.deploymentRequiresApproval?.toString() === 'true' || '', + ); ctx.output('envShortName', envShortName); const deploymentParametersArray = await getEnvDeploymentParameters(awsEnvEntity); @@ -205,21 +212,41 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { // looping over all providers of the selected environment for (const params of deploymentParametersArray) { - const { accountId, region, ssmAssumeRoleArn, ssmPathVpc, ssmPublicSubnets, ssmPrivateSubnets, ssmPathCluster, - envProviderName, envProviderType, envProviderPrefix, kubectlLambdaArn, kubectlLambdaRoleArn } = params; + const { + accountId, + region, + ssmAssumeRoleArn, + ssmPathVpc, + ssmPublicSubnets, + ssmPrivateSubnets, + ssmPathCluster, + envProviderName, + envProviderType, + envProviderPrefix, + kubectlLambdaArn, + kubectlLambdaRoleArn, + } = params; if (!accountId) { - throw new Error(`accountId not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`); + throw new Error( + `accountId not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`, + ); } if (!region) { - throw new Error(`region not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`); + throw new Error( + `region not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`, + ); } if (!ssmAssumeRoleArn) { - throw new Error(`ssmAssumeRoleArn not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`); + throw new Error( + `ssmAssumeRoleArn not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`, + ); } if (!ssmPathVpc) { - if ((envProviderType === 'ecs' || envProviderType === 'eks')) { - throw new Error(`ssmPathVpc not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`); + if (envProviderType === 'ecs' || envProviderType === 'eks') { + throw new Error( + `ssmPathVpc not configured for environment provider: ${envProviderName}. The provider IaC deployment may have failed.`, + ); } else { ctx.logger.info('No VPC configured for the environment provider'); } @@ -227,14 +254,29 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { // Get AWS credentials for the specific provider ctx.logger.info(`Getting credentials for AWS deployment to account ${accountId} in ${region}`); - const response = await getAWScreds(accountId, region, envProviderPrefix, envProviderName, ctx.user!.entity!); + const response = await getAWScreds( + config, + logger, + accountId, + region, + envProviderPrefix, + envProviderName, + ctx.user!.entity!, + ); const { credentials } = response; try { const vpcId = !!ssmPathVpc ? await getSSMParameterValue(region, credentials, ssmPathVpc, ctx.logger) : ''; - const publicSubnets = !!ssmPathVpc ? await getSSMParameterValue(region, credentials, ssmPublicSubnets, ctx.logger): ''; - const privateSubnets = !!ssmPathVpc ? await getSSMParameterValue(region, credentials, ssmPrivateSubnets, ctx.logger): ''; - const clusterArn = (envProviderType === 'ecs' || envProviderType === 'eks') ? await getSSMParameterValue(region, credentials, ssmPathCluster, ctx.logger) : ''; + const publicSubnets = !!ssmPathVpc + ? await getSSMParameterValue(region, credentials, ssmPublicSubnets, ctx.logger) + : ''; + const privateSubnets = !!ssmPathVpc + ? await getSSMParameterValue(region, credentials, ssmPrivateSubnets, ctx.logger) + : ''; + const clusterArn = + envProviderType === 'ecs' || envProviderType === 'eks' + ? await getSSMParameterValue(region, credentials, ssmPathCluster, ctx.logger) + : ''; const assumedRoleArn = await getSSMParameterValue(region, credentials, ssmAssumeRoleArn, ctx.logger); const envProvider: EnvironmentProvider = { @@ -259,7 +301,7 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { envProviderOutputArray.push(envProvider); } catch (err: any) { - throw new Error(`Failed to populate environment provider ${envProviderName}. ${err.toString()}`) + throw new Error(`Failed to populate environment provider ${envProviderName}. ${err.toString()}`); } } @@ -279,44 +321,40 @@ export function getEnvProvidersAction(options: { catalogClient: CatalogApi }) { const envProviderEntities = await catalogClient.getEntitiesByRefs({ entityRefs: envProvRefs }, { token }); - const deploymentParams: DeploymentParameters[] = envProviderEntities.items + return envProviderEntities.items .filter( entity => - entity && - ['name', 'envType', 'awsAccount', 'awsRegion', 'vpc'].every(key => key in entity.metadata), + entity && ['name', 'envType', 'awsAccount', 'awsRegion', 'vpc'].every(key => key in entity.metadata), ) .map(entity => { const { metadata } = entity!; const vpc = metadata.vpc?.toString() || ''; const deployParams: DeploymentParameters = { - envProviderPrefix: metadata['prefix']?.toString() || '', + envProviderPrefix: metadata.prefix?.toString() || '', envName: envEntity.metadata.name, envProviderName: metadata.name, envRef: environmentRef, - envProviderType: metadata['envType']?.toString().toLowerCase() || '', - accountId: metadata['awsAccount']?.toString() || '', - region: metadata['awsRegion']?.toString() || '', - ssmAssumeRoleArn: metadata['provisioningRole']?.toString() || '', + envProviderType: metadata.envType?.toString().toLowerCase() || '', + accountId: metadata.awsAccount?.toString() || '', + region: metadata.awsRegion?.toString() || '', + ssmAssumeRoleArn: metadata.provisioningRole?.toString() || '', ssmPathVpc: vpc, ssmPrivateSubnets: `${vpc}/private-subnets`, ssmPublicSubnets: `${vpc}/public-subnets`, - ssmPathCluster: metadata['clusterName']?.toString() || '', + ssmPathCluster: metadata.clusterName?.toString() || '', }; - if (metadata['kubectlLambdaArn']) { - deployParams.kubectlLambdaArn = metadata['kubectlLambdaArn'].toString(); + if (metadata.kubectlLambdaArn) { + deployParams.kubectlLambdaArn = metadata.kubectlLambdaArn.toString(); } - if (metadata['clusterAdminRole']) { - deployParams.kubectlLambdaRoleArn = metadata['clusterAdminRole'].toString(); + if (metadata.clusterAdminRole) { + deployParams.kubectlLambdaRoleArn = metadata.clusterAdminRole.toString(); } return deployParams; }); - - return deploymentParams; } }, }); - } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/index.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/index.ts index f04751d5..fc30b1b9 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/index.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-env-providers/index.ts @@ -1,4 +1,3 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { getEnvProvidersAction, } from './get-env-providers'; - +export { getEnvProvidersAction } from './get-env-providers'; diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/get-platform-metadata.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/get-platform-metadata.ts index c1f1eda3..2c0b6543 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/get-platform-metadata.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/get-platform-metadata.ts @@ -22,6 +22,7 @@ const examples = [ }, ]; +/** @public */ export function getPlatformMetadataAction(options: { envConfig: Config }) { const { envConfig } = options; @@ -32,20 +33,18 @@ export function getPlatformMetadataAction(options: { envConfig: Config }) { schema: { output: { type: 'object', - required: [ - 'platformRegion', - ], + required: ['platformRegion'], properties: { platformRegion: { title: 'The AWS region where the OPA on AWS solution is deployed', - type: 'string' - } - } - } + type: 'string', + }, + }, + }, }, async handler(ctx) { const platformRegion = envConfig.getString('backend.platformRegion'); ctx.output('platformRegion', platformRegion); }, }); -} \ No newline at end of file +} diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/index.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/index.ts index 8838afb1..2e38f313 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/index.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-metadata/index.ts @@ -1,4 +1,3 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 export { getPlatformMetadataAction } from './get-platform-metadata'; - diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-parameters/get-platform-parameters.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-parameters/get-platform-parameters.ts index a7c01f3f..78c85ad8 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-parameters/get-platform-parameters.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-platform-parameters/get-platform-parameters.ts @@ -3,15 +3,15 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; -import { - getPlatformAccountSSMParameterValue -} from '../../helpers/action-context'; -import { Config } from '@backstage/config' +import { getPlatformAccountSSMParameterValue } from '../../helpers/action-context'; +import { Config } from '@backstage/config'; + const ID = 'opa:get-platform-parameters'; const examples = [ { - description: 'Retrieve AWS SSM parameter values for the OPA on AWS platform so that their values can be used by other template actions', + description: + 'Retrieve AWS SSM parameter values for the OPA on AWS platform so that their values can be used by other template actions', example: yaml.stringify({ steps: [ { @@ -19,9 +19,8 @@ const examples = [ id: 'opaGetPlatformParams', name: 'Get parameter values', input: { - paramKeys: - - '/my/ssm/parameter', - region: 'us-east-1' + paramKeys: -'/my/ssm/parameter', + region: 'us-east-1', }, }, ], @@ -29,6 +28,7 @@ const examples = [ }, ]; +/** @public */ export function getPlatformParametersAction(options: { envConfig: Config }) { const { envConfig } = options; return createTemplateAction<{ @@ -46,7 +46,7 @@ export function getPlatformParametersAction(options: { envConfig: Config }) { paramKeys: { type: 'array', items: { - type: 'string' + type: 'string', }, title: 'SSM parameter keys', description: 'The SSM parameter keys to look up', @@ -54,15 +54,14 @@ export function getPlatformParametersAction(options: { envConfig: Config }) { region: { type: 'string', title: 'Platform region', - description: 'Optional region to locate SSM parameters. If not provided, the default region will be used where Backstage is running' - } + description: + 'Optional region to locate SSM parameters. If not provided, the default region will be used where Backstage is running', + }, }, }, output: { type: 'object', - required: [ - 'params', - ], + required: ['params'], properties: { params: { title: 'Map of SSM parameters', @@ -72,9 +71,11 @@ export function getPlatformParametersAction(options: { envConfig: Config }) { }, }, async handler(ctx) { - let { paramKeys, region } = ctx.input; + const { paramKeys } = ctx.input; + let { region } = ctx.input; + if (!region) { - region = envConfig.getString('backend.platformRegion') + region = envConfig.getString('backend.platformRegion'); } ctx.logger.info(`paramKeys: ${JSON.stringify(paramKeys)}`); ctx.logger.info(`Region: ${region}`); @@ -83,15 +84,15 @@ export function getPlatformParametersAction(options: { envConfig: Config }) { if (ctx.user?.entity === undefined) { // Verify the automationKey value. If it matches, set an automation user in the context if (ctx.secrets?.automationKey === process.env.AUTOMATION_KEY) { - console.log("Automation key provided to use automation user"); + console.log('Automation key provided to use automation user'); ctx.user = { entity: { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: 'automation' }, - spec: { profile: { displayName: "Automation User" } } - } - } + spec: { profile: { displayName: 'Automation User' } }, + }, + }; } else { ctx.logger.info(`No user context provided for ${ID} action`); throw new Error(`No user context provided for ${ID} action`); @@ -99,31 +100,30 @@ export function getPlatformParametersAction(options: { envConfig: Config }) { } // Get a key/value map of SSM parameters - const getEnvProviderSsmParams = async () - : Promise<{ [key: string]: string; }> => { - const params = (await Promise.all( - paramKeys.map(async (paramKey): Promise<{ [key: string]: string; }> => { - const val = await getPlatformAccountSSMParameterValue(paramKey, region, ctx.logger); - return { - [paramKey]: val - }; - }) - )).reduce((acc, paramKeyValMap) => { - const typedAcc: { [key: string]: string; } = acc; + const getEnvProviderSsmParams = async (): Promise<{ + [key: string]: string; + }> => { + return ( + await Promise.all( + paramKeys.map(async (paramKey): Promise<{ [key: string]: string }> => { + const val = await getPlatformAccountSSMParameterValue(envConfig, paramKey, region, ctx.logger); + return { + [paramKey]: val, + }; + }), + ) + ).reduce((acc, paramKeyValMap) => { + const typedAcc: { [key: string]: string } = acc; const key = Object.keys(paramKeyValMap)[0]; return { - ...typedAcc, [key]: paramKeyValMap[key] + ...typedAcc, + [key]: paramKeyValMap[key], }; }, {}); - - return params; - }; const envParams = await getEnvProviderSsmParams(); ctx.logger.info(JSON.stringify(envParams)); ctx.output('params', envParams); - }, }); - } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-ssm-parameters/get-ssm-parameters.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-ssm-parameters/get-ssm-parameters.ts index c24a9654..b3d92091 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-ssm-parameters/get-ssm-parameters.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/actions/get-ssm-parameters/get-ssm-parameters.ts @@ -10,11 +10,15 @@ import { } from '../../helpers/action-context'; import { EnvironmentProvider, EnvironmentProviderConnection } from '../../types'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; + const ID = 'opa:get-ssm-parameters'; const examples = [ { - description: 'Retreive AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions', + description: + 'Retrieve AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions', example: yaml.stringify({ steps: [ { @@ -23,8 +27,7 @@ const examples = [ name: 'Get parameter values', input: { envProviders: "${{ steps['opaGetAwsEnvProviders'].output.envProviders }}", - paramKeys: - - '/my/ssm/parameter' + paramKeys: -'/my/ssm/parameter', }, }, ], @@ -32,14 +35,15 @@ const examples = [ }, ]; -export function getSsmParametersAction() { - +/** @public */ +export function getSsmParametersAction(config: Config, logger: LoggerService) { return createTemplateAction<{ paramKeys: string[]; envProviders: EnvironmentProvider[]; }>({ id: ID, - description: 'Retreive AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions', + description: + 'Retrieve AWS SSM parameter values for each environment provider so that their configurations can be used by other template actions', examples, schema: { input: { @@ -49,7 +53,7 @@ export function getSsmParametersAction() { paramKeys: { type: 'array', items: { - type: 'string' + type: 'string', }, title: 'SSM parameter keys', description: 'The SSM parameter keys to look up', @@ -63,9 +67,7 @@ export function getSsmParametersAction() { }, output: { type: 'object', - required: [ - 'params', - ], + required: ['params'], properties: { params: { title: 'Map of SSM parameters, keyed off of the environment provider name', @@ -83,64 +85,75 @@ export function getSsmParametersAction() { if (ctx.user?.entity === undefined) { // Verify the automationKey value. If it matches, set an automation user in the context if (ctx.secrets?.automationKey === process.env.AUTOMATION_KEY) { - console.log("Automation key provided to use automation user"); + console.log('Automation key provided to use automation user'); ctx.user = { entity: { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: 'automation' }, - spec: { profile: { displayName: "Automation User" } } - } - } + spec: { profile: { displayName: 'Automation User' } }, + }, + }; } else { ctx.logger.info(`No user context provided for ${ID} action`); throw new Error(`No user context provided for ${ID} action`); } } - const providerConnect: EnvProviderConnectMap = - await getEnvironmentProviderConnectInfo(envProviders, ctx.user!.entity!); + const providerConnect: EnvProviderConnectMap = await getEnvironmentProviderConnectInfo( + config, + logger, + envProviders, + ctx.user!.entity!, + ); // Get a key/value map of SSM parameters for the supplied environment provider connection - const getEnvProviderSsmParams = async (connection: EnvironmentProviderConnection) - : Promise<{ [key: string]: string; }> => { - - const params = (await Promise.all( - paramKeys.map(async (paramKey): Promise<{ [key: string]: string; }> => { - const val = await getSSMParameterValue( - connection.region, connection.awsAuthResponse.credentials, paramKey, ctx.logger); - return { - [paramKey]: val - }; - }) - )).reduce((acc, paramKeyValMap) => { - const typedAcc: { [key: string]: string; } = acc; + const getEnvProviderSsmParams = async ( + connection: EnvironmentProviderConnection, + ): Promise<{ [key: string]: string }> => { + return ( + await Promise.all( + paramKeys.map(async (paramKey): Promise<{ [key: string]: string }> => { + const val = await getSSMParameterValue( + connection.region, + connection.awsAuthResponse.credentials, + paramKey, + ctx.logger, + ); + return { + [paramKey]: val, + }; + }), + ) + ).reduce((acc, paramKeyValMap) => { + const typedAcc: { [key: string]: string } = acc; const key = Object.keys(paramKeyValMap)[0]; return { - ...typedAcc, [key]: paramKeyValMap[key] + ...typedAcc, + [key]: paramKeyValMap[key], }; }, {}); - - return params; - }; - const paramsPerEnvProvider = (await Promise.all( - envProviders.map(async (envProvider: EnvironmentProvider) - : Promise<{ [key: string]: { [key: string]: string; }; }> => { - - const { envProviderName } = envProvider; - const envProviderConnection = providerConnect[envProviderName]; - const envParams = await getEnvProviderSsmParams(envProviderConnection); - return { - [envProviderName]: envParams - } - }) - )).reduce((acc, envProviderNameToSsmParamsMap) => { - const typedAcc: { [key: string]: { [key: string]: string; }; } = acc; + const paramsPerEnvProvider = ( + await Promise.all( + envProviders.map( + async (envProvider: EnvironmentProvider): Promise<{ [key: string]: { [key: string]: string } }> => { + const { envProviderName } = envProvider; + const envProviderConnection = providerConnect[envProviderName]; + const envParams = await getEnvProviderSsmParams(envProviderConnection); + return { + [envProviderName]: envParams, + }; + }, + ), + ) + ).reduce((acc, envProviderNameToSsmParamsMap) => { + const typedAcc: { [key: string]: { [key: string]: string } } = acc; const key = Object.keys(envProviderNameToSsmParamsMap)[0]; return { - ...typedAcc, [key]: envProviderNameToSsmParamsMap[key] + ...typedAcc, + [key]: envProviderNameToSsmParamsMap[key], }; }, {}); @@ -149,17 +162,16 @@ export function getSsmParametersAction() { const envProviderParamsMap = maskedValues[providerName]; Object.keys(envProviderParamsMap).forEach(ssmKey => { if (envProviderParamsMap[ssmKey]) { - envProviderParamsMap[ssmKey] = "masked"; + envProviderParamsMap[ssmKey] = 'masked'; } else { - envProviderParamsMap[ssmKey] = "blank or missing value"; + envProviderParamsMap[ssmKey] = 'blank or missing value'; } - }) - }) + }); + }); ctx.logger.info(`masked params: ${JSON.stringify(maskedValues, null, 2)}`); ctx.output('params', paramsPerEnvProvider); }, }); - } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/example/template.yaml b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/example/template.yaml index cbf20935..435aa5d7 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/example/template.yaml +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/example/template.yaml @@ -10,7 +10,7 @@ metadata: spec: owner: group:developers type: website - + parameters: - title: Provide basic component information required: @@ -49,7 +49,7 @@ spec: - AWSEnvironment defaultKind: AWSEnvironment - # Get the Gitlab repository for source code management + # Get the Gitlab repository for source code management - title: Choose a git repository location required: - repoUrl @@ -60,7 +60,7 @@ spec: ui:field: RepoUrlPicker ui:options: allowedHosts: - - {{ gitlab_hostname }} + - { { gitlab_hostname } } allowedOwners: - aws-app @@ -102,17 +102,17 @@ spec: input: secretName: aws-apps-${{ (parameters.repoUrl | parseRepoUrl).repo | lower }}-access-token - # Retrieves Infrastructure as Code (IaC) that should be executed (by the CICD pipeline) before the applicaiton + # Retrieves Infrastructure as Code (IaC) that should be executed (by the CICD pipeline) before the applicaiton # is deployed. The below example retrieves AWS ECS CDK code - id: fetchIac name: Fetch ECS Infrastructure as Code action: fetch:template - input: + input: url: https://{{ gitlab_hostname }}/opa-admin/backstage-reference/-/tree/main/common/aws_ecs targetPath: ./.iac values: component_id: ${{ parameters.component_id | lower }} - appEnvPlaintext: "" + appEnvPlaintext: '' # Fetches the template code from the remote repo where this template resides # In a real template, the input values which start with "aws_" would be used to @@ -157,7 +157,7 @@ spec: action: catalog:register input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} - catalogInfoPath: "/.backstage/catalog-info.yaml" + catalogInfoPath: '/.backstage/catalog-info.yaml' # Outputs are displayed to the user after a successful execution of the template. output: diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/action-context.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/action-context.ts index 0c5b8888..1cf788ef 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/action-context.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/action-context.ts @@ -7,12 +7,24 @@ import { Logger } from 'winston'; import { UserEntity } from '@backstage/catalog-model'; import { getAWScreds } from '@aws/plugin-aws-apps-backend-for-backstage'; import { EnvironmentProvider, EnvironmentProviderConnection } from '../types'; -import { SecretsManagerClient, CreateSecretCommandInput, CreateSecretCommand, PutSecretValueCommand, PutSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; - -export type EnvProviderConnectMap = { [key: string]: EnvironmentProviderConnection; } +import { + CreateSecretCommand, + CreateSecretCommandInput, + PutSecretValueCommand, + PutSecretValueCommandInput, + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; + +import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; + +export type EnvProviderConnectMap = { + [key: string]: EnvironmentProviderConnection; +}; /** - * Returns connection information (credentials, accountId, region) for each supplied + * Returns connection information (credentials, accountId, region) for each supplied * environment provider. * * @param envProviders - List of environment providers @@ -20,32 +32,48 @@ export type EnvProviderConnectMap = { [key: string]: EnvironmentProviderConnecti * @returns A map of connection information, keyed off of the environment provider name */ export async function getEnvironmentProviderConnectInfo( - envProviders: EnvironmentProvider[], userEntity?: UserEntity) - : Promise { - - const envProviderConnectionsMap = (await Promise.all( - envProviders.map(async (envProvider: EnvironmentProvider): Promise => { - const { accountId, region, envProviderPrefix, envProviderName } = envProvider; - const awsAuthResponse = await getAWScreds(accountId, region, envProviderPrefix, envProviderName, userEntity); - return { - providerName: envProvider.envProviderName, - accountId, - region, - awsAuthResponse, - } - }) - )).reduce((acc, envProviderConnection) => { + config: Config, + logger: LoggerService, + envProviders: EnvironmentProvider[], + userEntity?: UserEntity, +): Promise { + return ( + await Promise.all( + envProviders.map(async (envProvider: EnvironmentProvider): Promise => { + const { accountId, region, envProviderPrefix, envProviderName } = envProvider; + const awsAuthResponse = await getAWScreds( + config, + logger, + accountId, + region, + envProviderPrefix, + envProviderName, + userEntity, + ); + return { + providerName: envProvider.envProviderName, + accountId, + region, + awsAuthResponse, + }; + }), + ) + ).reduce((acc, envProviderConnection) => { const typedAcc: EnvProviderConnectMap = acc; return { - ...typedAcc, [envProviderConnection.providerName]: envProviderConnection + ...typedAcc, + [envProviderConnection.providerName]: envProviderConnection, }; }, {}); - - return envProviderConnectionsMap; } // Get the value for a specified SSM Parameter Store path -export async function getSSMParameterValue(region: string, creds: AwsCredentialIdentity, ssmPath: string, logger?: Logger): Promise { +export async function getSSMParameterValue( + region: string, + creds: AwsCredentialIdentity, + ssmPath: string, + logger?: Logger, +): Promise { const ssmClient = new SSMClient({ region, customUserAgent: 'opa-plugin', @@ -72,12 +100,19 @@ export async function getSSMParameterValue(region: string, creds: AwsCredentialI return ssmResponse.Parameter.Value; } - // Get the value for a specified SSM Parameter Store path -export async function getPlatformAccountSSMParameterValue(ssmPath: string, region?: string, logger?: Logger): Promise { +export async function getPlatformAccountSSMParameterValue( + config: Config, + ssmPath: string, + region?: string, + logger?: Logger, +): Promise { + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config); + const awsCredentialProvider = await awsCredentialsManager.getCredentialProvider({}); const ssmClient = new SSMClient({ region, customUserAgent: 'opa-plugin', + credentialDefaultProvider: () => awsCredentialProvider.sdkCredentialProvider, }); const ssmResponse = await ssmClient.send( new GetParameterCommand({ @@ -95,9 +130,13 @@ export async function getPlatformAccountSSMParameterValue(ssmPath: string, regio return ssmResponse.Parameter.Value; } -export async function createSecret(secretName: string, description: string, region?: string, - tags?: { Key: string, Value: string | number | boolean }[], logger?: Logger): Promise { - +export async function createSecret( + secretName: string, + description: string, + region?: string, + tags?: { Key: string; Value: string | number | boolean }[], + logger?: Logger, +): Promise { if (logger) { logger.debug('Calling create Secret'); } @@ -108,7 +147,7 @@ export async function createSecret(secretName: string, description: string, regi }); const client = new SecretsManagerClient({ - region + region, }); const params: CreateSecretCommandInput = { Name: secretName, @@ -131,13 +170,12 @@ export async function putSecret( region?: string, logger?: Logger, ): Promise { - if (logger) { logger.debug(`Updating secret ${secretArn}`); } const client = new SecretsManagerClient({ - region + region, }); const params: PutSecretValueCommandInput = { SecretId: secretArn, @@ -152,6 +190,6 @@ export async function putSecret( logger.error(error); logger.error('Error updating secret value'); } - return Promise.reject(new Error('Error updating secret value')); } + return Promise.reject(new Error('Error updating secret value')); } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/util.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/util.ts index 976497a1..91fcd80a 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/util.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/helpers/util.ts @@ -75,7 +75,7 @@ export const parseRepoUrl = (repoUrl: string, integrations: ScmIntegrationRegist * @public */ function checkRequiredParams(repoUrl: URL, ...params: string[]) { - for (let param of params) { + for (const param of params) { if (!repoUrl.searchParams.get(param)) { throw new InputError(`Invalid repo URL passed to publisher: ${repoUrl.toString()}, missing ${param}`); } diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/index.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/index.ts index 2f940356..f1dc360f 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/index.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/index.ts @@ -4,8 +4,8 @@ /***/ /** * The opa module for @backstage/plugin-scaffolder-backend. - * - * @packageDocumentation */ export * from './actions'; +export * from './types'; +export { scaffolderModuleAwsApps as default } from './module'; diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/module.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/module.ts new file mode 100644 index 00000000..bf3dd959 --- /dev/null +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/module.ts @@ -0,0 +1,44 @@ +import { coreServices, createBackendModule } from '@backstage/backend-plugin-api'; +import { CatalogClient } from '@backstage/catalog-client'; +import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { ScmIntegrations } from '@backstage/integration'; +import { + createRepoAccessTokenAction, + createSecretAction, + createS3BucketAction, + getEnvProvidersAction, + getComponentInfoAction, + getSsmParametersAction, + getPlatformParametersAction, + getPlatformMetadataAction, +} from './actions'; + +/** @public */ +export const scaffolderModuleAwsApps = createBackendModule({ + pluginId: 'scaffolder', // name of the plugin that the module is targeting + moduleId: 'aws-apps', + register(env) { + env.registerInit({ + deps: { + scaffolder: scaffolderActionsExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + }, + async init({ scaffolder, config, logger, discovery }) { + const integrations = ScmIntegrations.fromConfig(config); + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + scaffolder.addActions(createS3BucketAction()); + scaffolder.addActions(createSecretAction({ envConfig: config })); + scaffolder.addActions(getEnvProvidersAction({ config, logger, catalogClient })); + scaffolder.addActions(getComponentInfoAction()); + scaffolder.addActions(getSsmParametersAction(config, logger)); + scaffolder.addActions(getPlatformMetadataAction({ envConfig: config })); + scaffolder.addActions(getPlatformParametersAction({ envConfig: config })); + scaffolder.addActions(createRepoAccessTokenAction({ integrations, envConfig: config })); + }, + }); + }, +}); diff --git a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/types.ts b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/types.ts index 45144f8d..745b6ce7 100644 --- a/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/types.ts +++ b/backstage-plugins/plugins/scaffolder-backend-module-aws-apps/src/types.ts @@ -3,13 +3,15 @@ import { AwsAuthResponse } from '@aws/plugin-aws-apps-backend-for-backstage'; +/** @public */ export type EnvironmentProviderConnection = { providerName: string; accountId: string; region: string; awsAuthResponse: AwsAuthResponse; -} +}; +/** @public */ export type EnvironmentProvider = { envProviderName: string; envProviderType: string; @@ -23,4 +25,4 @@ export type EnvironmentProvider = { assumedRoleArn: string; kubectlLambdaArn?: string; kubectlLambdaRoleArn?: string; -} +};