diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1043e035..6f205feb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,6 +93,7 @@ test-app$ yarn watch | TEST_STACK_RETENTION_POLICY | After tests complete, `RETAIN`, `DELETE`, or `SELF_DESTRUCT` (delete after `TEST_SELF_DESTRUCT_DELAY_SECONDS`). Default: `SELF_DESTRUCT` | | TEST_SELF_DESTRUCT_DELAY_SECONDS | Number of seconds to wait before self destructing the stack, Default: 12 hours (43200s) | | CLEAN_UP_STACK | Deploys a CFN stack that can delete all of the test stacks (and itself) when invoked. Used the in Pull Request Closed CleanUp workflow to clean up after PRs. | +| TEST_DEPLOYER | Deploy using CDK/cloud formation (`CFN`) or using functionless's custom `NODE_CFN`, Default `CFN` | ### Website diff --git a/apps/fl-exp-simple/tsconfig.json b/apps/fl-exp-simple/tsconfig.json index abb692ef..b46352e3 100644 --- a/apps/fl-exp-simple/tsconfig.json +++ b/apps/fl-exp-simple/tsconfig.json @@ -6,10 +6,5 @@ "inlineSourceMap": true }, "include": ["."], - "exclude": ["lib", "test", "node_modules"], - "references": [ - { "path": "../../packages/@functionless/aws-constructs" }, - { "path": "../../packages/@functionless/aws" }, - { "path": "../../packages/@functionless/fl-exp" } - ] + "exclude": ["lib", "test", "node_modules"] } diff --git a/apps/test-app/src/order-processing-queue.ts b/apps/test-app/src/order-processing-queue.ts index 1ec08276..f9fe15cc 100644 --- a/apps/test-app/src/order-processing-queue.ts +++ b/apps/test-app/src/order-processing-queue.ts @@ -109,32 +109,32 @@ failedOrderQueue.messages().forEach(async (message) => { }); }); -// processedOrderQueue.messages().forEach((order) => { -// console.log("processed order", order); -// }); +processedOrderQueue.messages().forEach((order) => { + console.log("processed order", order); +}); -new StepFunction( - stack, - "SendMessageBatch", - async (input: { messages: OrderPlacedEvent[] }) => { - await orderQueue.sendMessageBatch({ - Entries: input.messages.map((message, idx) => ({ - Id: `${idx}`, - MessageBody: message, - })), - }); - } -); +// new StepFunction( +// stack, +// "SendMessageBatch", +// async (input: { messages: OrderPlacedEvent[] }) => { +// await orderQueue.sendMessageBatch({ +// Entries: input.messages.map((message, idx) => ({ +// Id: `${idx}`, +// MessageBody: message, +// })), +// }); +// } +// ); -new StepFunction( - stack, - "SendMessage", - async (input: { message: OrderPlacedEvent }) => { - await orderQueue.sendMessage({ - MessageBody: input.message, - }); - } -); +// new StepFunction( +// stack, +// "SendMessage", +// async (input: { message: OrderPlacedEvent }) => { +// await orderQueue.sendMessage({ +// MessageBody: input.message, +// }); +// } +// ); // TODO: implement retry logic once new intrinsics arrive // @see https://github.com/functionless/functionless/pull/468 @@ -188,7 +188,7 @@ export interface UserPass { const secret = new JsonSecret(stack, "JsonSecret", { secretStringValue: SecretValue.unsafePlainText( JSON.stringify({ - username: "sam", + username: "sam2", password: "sam", }) ), @@ -204,3 +204,12 @@ new Function(stack, "SecretFunc", async (input: "get" | UserPass) => { return response; } }); + +new JsonSecret(stack, "JsonSecret2", { + secretStringValue: SecretValue.unsafePlainText( + JSON.stringify({ + username: "sam", + password: "sam", + }) + ), +}); diff --git a/apps/test-app/tsconfig.json b/apps/test-app/tsconfig.json index 80e21001..80ca6e64 100644 --- a/apps/test-app/tsconfig.json +++ b/apps/test-app/tsconfig.json @@ -7,7 +7,8 @@ { "name": "@functionless/language-service" } - ] + ], + "types": ["node"] }, "include": ["src/**/*.ts"], "exclude": ["node_modules"], diff --git a/package.json b/package.json index 31793cd4..34205aa7 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,11 @@ }, "devDependencies": { "husky": "^8.0.1", - "lerna": "^5.5.4", + "lerna": "^5.6.1", "lint-staged": "^13.0.3", "turbo": "^1.5.5" + }, + "dependencies": { + "typescript": "^4.8.4" } } diff --git a/packages/@functionless/ast/src/ensure.ts b/packages/@functionless/ast/src/ensure.ts index f708e139..15ba252f 100644 --- a/packages/@functionless/ast/src/ensure.ts +++ b/packages/@functionless/ast/src/ensure.ts @@ -69,7 +69,7 @@ export function ensure( .map((kind) => typeof kind === "number" ? getNodeKindName(kind) : kind ) - .join(", ")}` + .join(", ")}, found: ${JSON.stringify(item)}` ); } } diff --git a/packages/@functionless/aws-constructs/package.json b/packages/@functionless/aws-constructs/package.json index 5e9d1022..6ecaab91 100644 --- a/packages/@functionless/aws-constructs/package.json +++ b/packages/@functionless/aws-constructs/package.json @@ -31,7 +31,9 @@ "@aws-cdk/cloudformation-diff": "2.44.0", "@aws-cdk/cx-api": "2.44.0", "@aws-sdk/client-dynamodb": "^3.180.0", + "@aws-sdk/client-ssm": "^3.180.0", "@functionless/jest": "^0.27.0", + "@functionless/formation": "*", "@swc/jest": "^0.2.22", "@types/fs-extra": "^9.0.13", "@types/jest": "^29.0.3", @@ -64,8 +66,6 @@ "graphql-tag": "^2.12.6", "jest": "^29.0.3", "jest-junit": "^13", - "json-schema": "^0.4.0", - "npm-check-updates": "^15", "prettier": "^2.7.1", "promptly": "^3.2.0", "proxy-agent": "^5.0.0", diff --git a/packages/@functionless/aws-constructs/test/queue.localstack.test.ts b/packages/@functionless/aws-constructs/test/queue.localstack.test.ts index e880dd3b..e32b48b6 100644 --- a/packages/@functionless/aws-constructs/test/queue.localstack.test.ts +++ b/packages/@functionless/aws-constructs/test/queue.localstack.test.ts @@ -1,12 +1,6 @@ import "jest"; -import { - aws_dynamodb, - aws_sqs, - CfnOutput, - Duration, - RemovalPolicy, -} from "aws-cdk-lib"; +import { aws_dynamodb, aws_sqs, Duration, RemovalPolicy } from "aws-cdk-lib"; import { Role } from "aws-cdk-lib/aws-iam"; import { SQSBatchResponse } from "aws-lambda"; import { Construct } from "constructs"; @@ -248,10 +242,7 @@ runtimeTestSuite<{ data: string; } - const addr = new CfnOutput(scope, "out", { value: "" }); - const bus = new EventBus>(scope, "bus", { - eventBusName: addr.node.addr, - }); + const bus = new EventBus>(scope, "bus"); bus.resource.grantPutEventsTo(testRole); @@ -273,7 +264,7 @@ runtimeTestSuite<{ return { queue, outputs: { - busName: addr.node.addr, + busName: bus.resource.eventBusName, }, }; }, @@ -288,10 +279,7 @@ runtimeTestSuite<{ data: string; } - const addr = new CfnOutput(scope, "out", { value: "" }); - const bus = new EventBus>(scope, "bus", { - eventBusName: addr.node.addr, - }); + const bus = new EventBus>(scope, "bus"); bus.resource.grantPutEventsTo(testRole); @@ -334,7 +322,7 @@ runtimeTestSuite<{ return { queue, outputs: { - busName: addr.node.addr, + busName: bus.resource.eventBusName, messageGroupId: "busGroup", }, }; diff --git a/packages/@functionless/aws-constructs/test/runtime.ts b/packages/@functionless/aws-constructs/test/runtime.ts index 01ac29ab..234d53b5 100644 --- a/packages/@functionless/aws-constructs/test/runtime.ts +++ b/packages/@functionless/aws-constructs/test/runtime.ts @@ -1,8 +1,11 @@ import * as cxapi from "@aws-cdk/cx-api"; +import * as ssm from "@aws-sdk/client-ssm"; +import * as node_cfn from "@functionless/formation"; import { App, CfnOutput, Stack } from "aws-cdk-lib"; import { ArnPrincipal, Role } from "aws-cdk-lib/aws-iam"; import { SdkProvider } from "aws-cdk/lib/api/aws-auth"; import { CloudFormationDeployments } from "aws-cdk/lib/api/cloudformation-deployments"; +import { DeployStackResult } from "aws-cdk/lib/api/deploy-stack"; // eslint-disable-next-line import/no-extraneous-dependencies import AWS, { DynamoDB, @@ -27,6 +30,7 @@ export interface RuntimeTestExecutionContext { selfDestructProps: SelfDestructorProps; stackRetentionPolicy: "RETAIN" | "DELETE" | "SELF_DESTRUCT"; deployTarget: "AWS" | "LOCALSTACK"; + deployer: "CFN" | "NODE_CFN"; cleanUpStack: boolean; } @@ -52,6 +56,7 @@ export const runtimeTestExecutionContext: RuntimeTestExecutionContext = { : "SELF_DESTRUCT")) as RuntimeTestExecutionContext["stackRetentionPolicy"], // AWS | LOCALSTACK ; default: LOCALSTACK deployTarget: deploymentTarget as RuntimeTestExecutionContext["deployTarget"], + deployer: (process.env.TEST_DEPLOYER as "CFN" | "NODE_CFN") ?? "CFN", cleanUpStack: process.env.CLEAN_UP_STACK === "1" ? true : false, }; @@ -288,6 +293,9 @@ export function runtimeTestSuite< >) | undefined = undefined; + let synthTime: number = 0; + let deploymentTime: number = 0; + beforeAll(async () => { // resolve account and arn of current credentials const caller = await sts.getCallerIdentity().promise(); @@ -321,25 +329,40 @@ export function runtimeTestSuite< (t) => ("error" in t && t.error) || ("skip" in t && t.skip) ); if (!allErrored) { + const startSynth = new Date(); const cloudAssembly = await asyncSynth(app); + synthTime = new Date().getTime() - startSynth.getTime(); + const assetManifestArtifact = cloudAssembly.tryGetArtifact( + `${stack.artifactId}.assets` + ) as unknown as cxapi.AssetManifestArtifact; stackArtifact = cloudAssembly.getStackArtifact( stack.artifactId ) as unknown as cxapi.CloudFormationStackArtifact; - // Inspiration for the current approach: https://github.com/aws/aws-cdk/pull/18667#issuecomment-1075348390 - // Writeup on performance improvements: https://github.com/functionless/functionless/pull/184#issuecomment-1144767427 - const deployOut = await getCfnClient().then((client) => - client.deployStack({ - stack: stackArtifact!, - tags: Object.entries(stack.tags.tagValues()).map(([k, v]) => ({ - Key: k, - Value: v, - })), - // hotswap uses the current user's role and not the bootstrapped role. - // the CI user does not have all of the right permissions. - hotswap: !process.env.CI, - }) - ); + const startDeploy = new Date(); + + let deployOut: DeployStackResult | node_cfn.StackState; + if (runtimeTestExecutionContext.deployer === "CFN") { + // Inspiration for the current approach: https://github.com/aws/aws-cdk/pull/18667#issuecomment-1075348390 + // Writeup on performance improvements: https://github.com/functionless/functionless/pull/184#issuecomment-1144767427 + deployOut = await getCfnClient().then((client) => + client.deployStack({ + stack: stackArtifact!, + tags: Object.entries(stack.tags.tagValues()).map(([k, v]) => ({ + Key: k, + Value: v, + })), + // hotswap uses the current user's role and not the bootstrapped role. + // the CI user does not have all of the right permissions. + hotswap: !process.env.CI, + }) + ); + } else { + deployOut = await formationDeploy(stackArtifact, assetManifestArtifact); + } + deploymentTime = new Date().getTime() - startDeploy.getTime(); + + console.log(deployOut.outputs); const testRoleArn = deployOut.outputs[stack.resolve(testArnOutput.logicalId)]; @@ -385,18 +408,46 @@ export function runtimeTestSuite< deployOutputs: s, })); } + + async function formationDeploy( + stackArtifact: cxapi.CloudFormationStackArtifact, + assetManifest: cxapi.AssetManifestArtifact + ) { + const caller = await sts.getCallerIdentity().promise(); + console.log(JSON.stringify(stackArtifact.template, null, 2)); + const stack = new node_cfn.Stack({ + account: caller.Account!, + region: clientConfig.region!, + stackName: stackArtifact.stackName, + // @ts-ignore + ssmClient: new ssm.SSMClient({ + credentials: clientConfig.credentials!, + region: clientConfig.region!, + }), + sdkConfig: clientConfig, + }); + + const result = await stack.updateStack( + stackArtifact.template, + undefined, + assetManifest.file + ); + + return result; + } }); afterAll(async () => { + console.log("Synth Time:", synthTime, "DeployTime:", deploymentTime); if ( stackArtifact && runtimeTestExecutionContext.stackRetentionPolicy === "DELETE" ) { - await getCfnClient().then((client) => - client.destroyStack({ - stack: stackArtifact!, - }) - ); + // await getCfnClient().then((client) => + // client.destroyStack({ + // stack: stackArtifact!, + // }) + // ); } }); diff --git a/packages/@functionless/aws-constructs/tsconfig.json b/packages/@functionless/aws-constructs/tsconfig.json index 57343db0..1f063e4d 100644 --- a/packages/@functionless/aws-constructs/tsconfig.json +++ b/packages/@functionless/aws-constructs/tsconfig.json @@ -6,5 +6,13 @@ "types": ["node"] }, "include": ["src/**/*.ts", "src/**/*.json"], - "exclude": [] + "exclude": [], + "references": [ + { + "path": "../formation" + }, + { + "path": "../ast" + } + ] } diff --git a/packages/@functionless/formation/README.md b/packages/@functionless/formation/README.md new file mode 100644 index 00000000..4b225061 --- /dev/null +++ b/packages/@functionless/formation/README.md @@ -0,0 +1,63 @@ +# formation + +This is a toy-implementation of the AWS CloudFormation deployment engine in TypeScript and Node-JS. It was built as an experiment to better understand why CloudFormation is so slow and to also experiment locally with new features. + +It is built on top of the [AWS Cloud Control API](https://aws.amazon.com/cloudcontrolapi/) and thus only supports the [resources that are supported by the Cloud Control API](https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html). + +The most important missing feature right now is Rollbacks, so this tool should not be considered useful for any type of production service. The hope is that it can become a playground for CloudFormation enhancements and to stand as a performance benchmark for the official AWS CloudFormation service. I dream of the day when CloudFormation is as fast as Terraform and Pulumi's provisioning engines. + +## Supported Features + +- [x] Stack Create, Update and Delete +- [x] Stack Parameters +- [x] Resource Conditions +- [x] Intrinsic Functions (`Ref`, `!Ref`, `Fn::GetAtt`, `Fn::Join`, `Fn::Split`, `Fn::Select`, `Fn::FindInMap`, `Fn::Sub`, etc.) +- [ ] Rollbacks on failure +- [ ] Outputs and cross-stack references +- [ ] Assets + +## Usage + +```ts +import { CloudFormationTemplate, Stack } from "@functionless/formation"; + +const deployer = new Stack({ + account: "", + region: "", + stackName: "my-stack", +}); + +const template: CloudFormationTemplate = { + AWSTemplateFormatVersion: "2010-09-09", + Parameters: { + ShardCount: { + Type: "Number", + MinValue: 1, + }, + }, + Resources: { + MyStream: { + Type: "AWS::Kinesis::Stream", + Properties: { + Name: "MyKinesisStream", + RetentionPeriodHours: 168, + ShardCount: { + Ref: "ShardCount", + }, + Tags: [ + { + Key: "Environment", + Value: "Production", + }, + ], + }, + }, + }, +}; + +let state = await deployer.updateStack(template, { + ShardCount: 1, +}); + +state = await deployer.updateStack(newState); +``` diff --git a/packages/@functionless/formation/bin/node-cfn.js b/packages/@functionless/formation/bin/node-cfn.js new file mode 100755 index 00000000..c02e5401 --- /dev/null +++ b/packages/@functionless/formation/bin/node-cfn.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require("../lib/cli"); \ No newline at end of file diff --git a/packages/@functionless/formation/package.json b/packages/@functionless/formation/package.json new file mode 100644 index 00000000..5650e34a --- /dev/null +++ b/packages/@functionless/formation/package.json @@ -0,0 +1,107 @@ +{ + "name": "@functionless/formation", + "bin": { + "node-cfn": "bin/node-cfn.js", + "node-cfn.js": "bin/node-cfn.js" + }, + "scripts": { + "build": "tsc --build", + "lint": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test", + "test": "jest --all", + "test:update": "jest --updateSnapshot", + "test:watch": "jest --watch", + "typecheck": "tsc -p ./tsconfig.dev.json --noEmit", + "dev": "tsc --build -w" + }, + "devDependencies": { + "@types/jest": "^29.0.3", + "@types/node": "^16", + "@types/cli-table": "^0.3.1", + "@typescript-eslint/eslint-plugin": "^5", + "@typescript-eslint/parser": "^5", + "eslint": "^8", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^2.5.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^29.0.3", + "jest-junit": "^13", + "prettier": "^2.5.1", + "projen": "^0.51.11", + "standard-version": "^9", + "ts-jest": "^29.0.2", + "ts-node": "^10.4.0", + "typedoc": "^0.21.4", + "typescript": "^4.8.3" + }, + "dependencies": { + "@aws-sdk/client-cloudcontrol": "^3.180.0", + "@aws-sdk/client-eventbridge": "^3.180.0", + "@aws-sdk/client-iam": "^3.180.0", + "@aws-sdk/client-lambda": "^3.180.0", + "@aws-sdk/client-secrets-manager": "^3.180.0", + "@aws-sdk/client-sqs": "^3.180.0", + "@aws-sdk/client-ssm": "^3.180.0", + "@aws-sdk/client-sts": "^3.180.0", + "aws-sdk": "^2.1225.0", + "cdk-assets": "^2.43.1", + "chalk": "4", + "commander": "*", + "cli-table": "^0.3.11", + "fast-json-patch": "^3.1.0", + "short-uuid": "^4.2.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "main": "lib/index.js", + "license": "Apache-2.0", + "version": "0.0.0", + "jest": { + "collectCoverage": false, + "coveragePathIgnorePatterns": [ + "/test/", + "/node_modules/", + "/lib" + ], + "testMatch": [ + "/src/**/__tests__/**/*.ts?(x)", + "/(test|src)/**/*(*.)@(spec|test).ts?(x)" + ], + "clearMocks": true, + "coverageReporters": [ + "json", + "lcov", + "clover", + "cobertura", + "text" + ], + "coverageDirectory": "coverage", + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "watchPathIgnorePatterns": [ + "/node_modules/" + ], + "reporters": [ + "default", + [ + "jest-junit", + { + "outputDirectory": "test-reports" + } + ] + ], + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "tsconfig": "./tsconfig.dev.json" + } + ] + } + }, + "types": "lib/index.d.ts", + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@functionless/formation/src/asserts.ts b/packages/@functionless/formation/src/asserts.ts new file mode 100644 index 00000000..321a97ea --- /dev/null +++ b/packages/@functionless/formation/src/asserts.ts @@ -0,0 +1,26 @@ +export function assertIsString( + string: any, + argumentName: string +): asserts string is string { + if (typeof string !== "string") { + throw new Error( + `The ${argumentName} must be a string, but was ${typeof string}` + ); + } +} + +export function assertIsListOfStrings( + strings: any, + argumentName: string +): asserts strings is string[] { + if ( + !Array.isArray(strings) || + strings.find((s) => typeof s !== "string") !== undefined + ) { + throw new Error( + `The ${argumentName} argument must be a list of strings, but was ${typeof strings}` + ); + } else if (strings.length === 0) { + throw new Error(`The ${argumentName} cannot be empty.`); + } +} diff --git a/packages/@functionless/formation/src/aws.ts b/packages/@functionless/formation/src/aws.ts new file mode 100644 index 00000000..67b765ba --- /dev/null +++ b/packages/@functionless/formation/src/aws.ts @@ -0,0 +1,109 @@ +import { Account, ClientOptions, IAws } from "cdk-assets"; +import s3_v2 from "aws-sdk/clients/s3"; +import secrets_manager_v2 from "aws-sdk/clients/secretsmanager"; +import ecr_v2 from "aws-sdk/clients/ecr"; +import sts from "@aws-sdk/client-sts"; +import * as os from "os"; +import { ChainableTemporaryCredentials, Credentials } from "aws-sdk"; + +export default class AwsClient implements IAws { + constructor( + private account: string, + private region: string, + private sdkConfig?: any + ) {} + + async discoverPartition(): Promise { + return "aws"; + } + async discoverDefaultRegion(): Promise { + return this.region; + } + async discoverCurrentAccount(): Promise { + return { + accountId: this.account, + partition: await this.discoverPartition(), + }; + } + async discoverTargetAccount(options: ClientOptions): Promise { + const stsClient = await this.stsClient(await this.awsOptions(options)); + const response = await stsClient.send(new sts.GetCallerIdentityCommand({})); + if (!response.Account || !response.Arn) { + throw new Error( + `Unrecognized response from STS: '${JSON.stringify(response)}'` + ); + } + return { + accountId: response.Account!, + partition: response.Arn!.split(":")[1]!, + }; + } + async s3Client(options: ClientOptions): Promise { + return new s3_v2(options); + } + async stsClient(options: ClientOptions): Promise { + return new sts.STSClient(options); + } + async ecrClient(options: ClientOptions): Promise { + return new ecr_v2(options); + } + async secretsManagerClient( + options: ClientOptions + ): Promise { + return new secrets_manager_v2(await this.awsOptions(options)); + } + async awsOptions(options: ClientOptions) { + let credentials; + if (options.assumeRoleArn) { + credentials = await this.assumeRole( + options.region, + options.assumeRoleArn, + options.assumeRoleExternalId + ); + } + return { + ...this.sdkConfig, + region: options.region, + customUserAgent: "formation", + credentials, + }; + } + /** + * Explicit manual AssumeRole call + * + * Necessary since I can't seem to get the built-in support for ChainableTemporaryCredentials to work. + * + * It needs an explicit configuration of `masterCredentials`, we need to put + * a `DefaultCredentialProverChain()` in there but that is not possible. + */ + async assumeRole( + region: string | undefined, + roleArn: string, + externalId?: string + ): Promise { + return new ChainableTemporaryCredentials({ + params: { + RoleArn: roleArn, + ExternalId: externalId, + RoleSessionName: `formation-${safeUsername()}`, + }, + stsConfig: { + region, + customUserAgent: "formation", + }, + }); + } +} + +/** + * Return the username with characters invalid for a RoleSessionName removed + * + * @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters + */ +function safeUsername() { + try { + return os.userInfo().username.replace(/[^\w+=,.@-]/g, "@"); + } catch (e) { + return "noname"; + } +} diff --git a/packages/@functionless/formation/src/cli.ts b/packages/@functionless/formation/src/cli.ts new file mode 100644 index 00000000..e8f2a95f --- /dev/null +++ b/packages/@functionless/formation/src/cli.ts @@ -0,0 +1,176 @@ +import { program } from "commander"; +import fs from "fs/promises"; +import { + displayTopoEntries, + displayTopoOrder, + TopoDisplayEntry, +} from "./display"; +import { Stack, StackState } from "./stack"; +import { CloudFormationTemplate } from "./template"; +import * as sts from "@aws-sdk/client-sts"; + +const STS = new sts.STSClient({}); + +program + .command("show") + .argument("