From 98b17069402db94f2b5c5f20bcaa74de9bbb3935 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen <115044274+hoangnbn@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:59:50 -0700 Subject: [PATCH] Add amplify sandbox secret CLI (#250) --- .changeset/clever-cats-smoke.md | 7 + package-lock.json | 134 ++++++++++-------- packages/backend-secret/API.md | 5 +- packages/backend-secret/src/secret.ts | 7 +- .../backend-secret/src/ssm_secret.test.ts | 24 +++- packages/backend-secret/src/ssm_secret.ts | 24 ++-- .../lambda/backend_secret_fetcher.test.ts | 2 +- packages/cli/package.json | 1 + .../cli/src/commands/printer/.eslintrc.json | 5 + packages/cli/src/commands/printer/printer.ts | 31 ++++ .../src/commands/prompter/amplify_prompts.ts | 15 +- .../sandbox_delete_command.test.ts | 10 +- .../sandbox/sandbox-secret/constants.ts | 1 + .../sandbox_secret_command.test.ts | 52 +++++++ .../sandbox-secret/sandbox_secret_command.ts | 56 ++++++++ .../sandbox_secret_command_factory.test.ts | 35 +++++ .../sandbox_secret_command_factory.ts | 45 ++++++ .../sandbox_secret_get_command.test.ts | 92 ++++++++++++ .../sandbox_secret_get_command.ts | 64 +++++++++ .../sandbox_secret_list_command.test.ts | 68 +++++++++ .../sandbox_secret_list_command.ts | 43 ++++++ .../sandbox_secret_remove_command.test.ts | 70 +++++++++ .../sandbox_secret_remove_command.ts | 63 ++++++++ .../sandbox_secret_set_command.test.ts | 94 ++++++++++++ .../sandbox_secret_set_command.ts | 65 +++++++++ .../commands/sandbox/sandbox_command.test.ts | 10 +- .../src/commands/sandbox/sandbox_command.ts | 5 +- .../sandbox/sandbox_command_factory.ts | 10 +- packages/cli/tsconfig.json | 1 + 29 files changed, 938 insertions(+), 101 deletions(-) create mode 100644 .changeset/clever-cats-smoke.md create mode 100644 packages/cli/src/commands/printer/.eslintrc.json create mode 100644 packages/cli/src/commands/printer/printer.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/constants.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts create mode 100644 packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts diff --git a/.changeset/clever-cats-smoke.md b/.changeset/clever-cats-smoke.md new file mode 100644 index 0000000000..f5f4278241 --- /dev/null +++ b/.changeset/clever-cats-smoke.md @@ -0,0 +1,7 @@ +--- +'@aws-amplify/backend-secret': minor +'@aws-amplify/backend-cli': minor +'@aws-amplify/backend': patch +--- + +Provides sandbox secret CLI commands diff --git a/package-lock.json b/package-lock.json index a269c3fb27..564cbc6092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12086,6 +12086,7 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -12698,6 +12699,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, "engines": { "node": ">= 4" } @@ -13588,6 +13590,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -15387,6 +15390,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -16122,6 +16126,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -16136,6 +16141,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16146,7 +16152,8 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/send": { "version": "0.18.0", @@ -17779,6 +17786,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -18498,6 +18506,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, "engines": { "node": ">= 14" } @@ -18627,47 +18636,47 @@ }, "packages/auth-construct": { "name": "@aws-amplify/auth-construct-alpha", - "version": "0.2.0-alpha.5", + "version": "0.2.0-alpha.6", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/backend-output-storage": "^0.1.0" + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/backend-output-storage": "^0.1.1-alpha.0" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.4", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } }, "packages/backend": { "name": "@aws-amplify/backend", - "version": "0.1.1-alpha.3", + "version": "0.2.0-alpha.4", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/backend-output-storage": "^0.1.0", - "@aws-amplify/backend-secret": "^0.1.0" + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/backend-output-storage": "^0.1.1-alpha.0", + "@aws-amplify/backend-secret": "^0.2.0-alpha.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.119", "aws-lambda": "^1.0.7" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.3", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } }, "packages/backend-auth": { "name": "@aws-amplify/backend-auth", - "version": "0.2.0-alpha.3", + "version": "0.2.0-alpha.4", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.2.0-alpha.5", - "@aws-amplify/backend-output-storage": "0.1.0" + "@aws-amplify/auth-construct-alpha": "^0.2.0-alpha.6", + "@aws-amplify/backend-output-storage": "0.1.1-alpha.0" }, "devDependencies": { - "@aws-amplify/backend": "^0.1.1-alpha.2" + "@aws-amplify/backend": "^0.2.0-alpha.4" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.4", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } @@ -18686,43 +18695,43 @@ }, "packages/backend-function": { "name": "@aws-amplify/backend-function", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "dependencies": { - "@aws-amplify/backend-output-storage": "0.1.0", + "@aws-amplify/backend-output-storage": "0.1.1-alpha.0", "@aws-amplify/function-construct-alpha": "^0.1.1-alpha.1", "execa": "^7.1.1" }, "devDependencies": { - "@aws-amplify/backend": "^0.1.1-alpha.2" + "@aws-amplify/backend": "^0.2.0-alpha.4" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.2", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } }, "packages/backend-graphql": { "name": "@aws-amplify/backend-graphql", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/backend-output-storage": "0.1.0", + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/backend-output-storage": "0.1.1-alpha.0", "@aws-amplify/graphql-construct-alpha": "^0.6.2" }, "devDependencies": { - "@aws-amplify/backend": "^0.1.1-alpha.3" + "@aws-amplify/backend": "^0.2.0-alpha.4" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.3", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } }, "packages/backend-output-schemas": { "name": "@aws-amplify/backend-output-schemas", - "version": "0.2.0-alpha.2", + "version": "0.2.0-alpha.3", "devDependencies": { - "@aws-amplify/plugin-types": "0.1.1-alpha.4" + "@aws-amplify/plugin-types": "0.1.1-alpha.5" }, "peerDependencies": { "zod": "^3.21.4" @@ -18730,9 +18739,9 @@ }, "packages/backend-output-storage": { "name": "@aws-amplify/backend-output-storage", - "version": "0.1.0", + "version": "0.1.1-alpha.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2" + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3" }, "peerDependencies": { "aws-cdk-lib": "^2.80.0" @@ -18740,7 +18749,7 @@ }, "packages/backend-secret": { "name": "@aws-amplify/backend-secret", - "version": "0.1.0", + "version": "0.2.0-alpha.0", "dependencies": { "@aws-sdk/client-ssm": "^3.398.0" }, @@ -18748,35 +18757,36 @@ "@aws-sdk/types": "^3.370.0" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.0", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0" } }, "packages/backend-storage": { "name": "@aws-amplify/backend-storage", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "dependencies": { - "@aws-amplify/backend-output-storage": "^0.1.0", - "@aws-amplify/storage-construct-alpha": "^0.1.1-alpha.2" + "@aws-amplify/backend-output-storage": "^0.1.1-alpha.0", + "@aws-amplify/storage-construct-alpha": "^0.1.1-alpha.4" }, "devDependencies": { - "@aws-amplify/backend": "^0.1.1-alpha.2" + "@aws-amplify/backend": "^0.2.0-alpha.4" }, "peerDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.2", + "@aws-amplify/plugin-types": "^0.1.1-alpha.5", "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" } }, "packages/cli": { "name": "@aws-amplify/backend-cli", - "version": "0.2.0-alpha.3", + "version": "0.2.0-alpha.4", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/client-config": "^0.2.0-alpha.5", - "@aws-amplify/deployed-backend-client": "^0.1.0", - "@aws-amplify/model-generator": "^0.2.0-alpha.1", - "@aws-amplify/sandbox": "^0.1.1-alpha.5", + "@aws-amplify/backend-secret": "^0.2.0-alpha.0", + "@aws-amplify/client-config": "^0.2.0-alpha.6", + "@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0", + "@aws-amplify/model-generator": "^0.2.0-alpha.2", + "@aws-amplify/sandbox": "^0.2.0-alpha.6", "@aws-sdk/credential-providers": "^3.360.0", "@inquirer/prompts": "^3.0.0", "execa": "^7.2.0", @@ -18899,10 +18909,10 @@ }, "packages/client-config": { "name": "@aws-amplify/client-config", - "version": "0.2.0-alpha.5", + "version": "0.2.0-alpha.6", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0", "@aws-sdk/client-amplify": "^3.376.0", "@aws-sdk/client-cloudformation": "^3.376.0", "@aws-sdk/client-ssm": "^3.398.0", @@ -18914,7 +18924,7 @@ } }, "packages/create-amplify": { - "version": "0.2.0-alpha.5", + "version": "0.2.0-alpha.6", "dependencies": { "execa": "^7.2.0" }, @@ -18927,9 +18937,9 @@ }, "packages/deployed-backend-client": { "name": "@aws-amplify/deployed-backend-client", - "version": "0.1.0", + "version": "0.2.0-alpha.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", "@aws-sdk/client-amplify": "^3.414.0", "@aws-sdk/client-cloudformation": "^3.414.0", "@aws-sdk/types": "^3.413.0", @@ -18938,7 +18948,7 @@ }, "packages/form-generator": { "name": "@aws-amplify/form-generator", - "version": "0.1.1-alpha.0", + "version": "0.2.0-alpha.1", "dependencies": { "@aws-amplify/appsync-modelgen-plugin": "^2.6.0", "@aws-amplify/codegen-ui": "^2.15.8", @@ -18964,11 +18974,11 @@ }, "packages/integration-tests": { "name": "@aws-amplify/integration-tests", - "version": "0.2.0-alpha.3", + "version": "0.2.0-alpha.4", "devDependencies": { - "@aws-amplify/backend": "0.1.1-alpha.3", - "@aws-amplify/backend-auth": "0.2.0-alpha.3", - "@aws-amplify/backend-storage": "0.1.1-alpha.2", + "@aws-amplify/backend": "0.2.0-alpha.4", + "@aws-amplify/backend-auth": "0.2.0-alpha.4", + "@aws-amplify/backend-storage": "0.1.1-alpha.3", "execa": "^8.0.1", "fs-extra": "^11.1.1", "glob": "^10.2.7" @@ -19029,10 +19039,10 @@ }, "packages/model-generator": { "name": "@aws-amplify/model-generator", - "version": "0.2.0-alpha.1", + "version": "0.2.0-alpha.2", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0", "@aws-amplify/graphql-generator": "^0.1.0", "@aws-sdk/client-appsync": "^3.398.0", "@aws-sdk/client-s3": "^3.414.0", @@ -19042,7 +19052,7 @@ }, "packages/plugin-types": { "name": "@aws-amplify/plugin-types", - "version": "0.1.1-alpha.4", + "version": "0.1.1-alpha.5", "peerDependencies": { "aws-cdk-lib": "~2.80.0", "constructs": "^10.0.0" @@ -19050,11 +19060,11 @@ }, "packages/sandbox": { "name": "@aws-amplify/sandbox", - "version": "0.1.1-alpha.5", + "version": "0.2.0-alpha.6", "dependencies": { "@aws-amplify/backend-deployer": "0.1.1-alpha.3", - "@aws-amplify/client-config": "0.2.0-alpha.5", - "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/client-config": "0.2.0-alpha.6", + "@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0", "@aws-sdk/credential-providers": "^3.382.0", "@aws-sdk/types": "^3.378.0", "@parcel/watcher": "^2.3.0", @@ -19071,13 +19081,13 @@ }, "packages/storage-construct": { "name": "@aws-amplify/storage-construct-alpha", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.2", - "@aws-amplify/backend-output-storage": "^0.1.0" + "@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3", + "@aws-amplify/backend-output-storage": "^0.1.1-alpha.0" }, "devDependencies": { - "@aws-amplify/plugin-types": "^0.1.1-alpha.3" + "@aws-amplify/plugin-types": "^0.1.1-alpha.5" }, "peerDependencies": { "aws-cdk-lib": "~2.80.0", diff --git a/packages/backend-secret/API.md b/packages/backend-secret/API.md index 521d05c55a..ba7655b85f 100644 --- a/packages/backend-secret/API.md +++ b/packages/backend-secret/API.md @@ -14,8 +14,7 @@ import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types'; export const getSecretClient: (credentialProvider?: AwsCredentialIdentityProvider) => SecretClient; // @public -export type Secret = { - secretIdentifier: SecretIdentifier; +export type Secret = SecretIdentifier & { value: string; }; @@ -24,7 +23,7 @@ export type SecretAction = 'GET' | 'SET' | 'REMOVE' | 'LIST'; // @public export type SecretClient = { - getSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier) => Promise; + getSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier) => Promise; listSecrets: (backendIdentifier: UniqueBackendIdentifier | BackendId) => Promise; setSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretName: string, secretValue: string) => Promise; removeSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretName: string) => Promise; diff --git a/packages/backend-secret/src/secret.ts b/packages/backend-secret/src/secret.ts index e397db3a5d..60f0297492 100644 --- a/packages/backend-secret/src/secret.ts +++ b/packages/backend-secret/src/secret.ts @@ -15,10 +15,7 @@ export type SecretIdentifier = { /** * The secret object. */ -export type Secret = { - secretIdentifier: SecretIdentifier; - value: string; -}; +export type Secret = SecretIdentifier & { value: string }; /** * The client to manage backend secret. @@ -30,7 +27,7 @@ export type SecretClient = { getSecret: ( backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier - ) => Promise; + ) => Promise; /** * List secrets. diff --git a/packages/backend-secret/src/ssm_secret.test.ts b/packages/backend-secret/src/ssm_secret.test.ts index b027d8506f..c2ff3ca778 100644 --- a/packages/backend-secret/src/ssm_secret.test.ts +++ b/packages/backend-secret/src/ssm_secret.test.ts @@ -33,7 +33,7 @@ const testSecretIdWithVersion: SecretIdentifier = { }; const testSecret: Secret = { - secretIdentifier: testSecretIdWithVersion, + ...testSecretIdWithVersion, value: testSecretValue, }; @@ -50,7 +50,7 @@ void describe('SSMSecret', () => { Promise.resolve({ $metadata: {}, Parameter: { - Name: testSecretName, + Name: testBranchSecretFullNamePath, Value: testSecretValue, Version: testSecretVersion, }, @@ -96,6 +96,25 @@ void describe('SSMSecret', () => { }); }); + void it('gets undefined secret value', async () => { + mock.method(ssmClient, 'getParameter', () => + Promise.resolve({ + $metadata: {}, + Parameter: { + Name: testBranchSecretFullNamePath, + Version: testSecretVersion, + }, + }) + ); + const expectedErr = new SecretError( + `The value of secret '${testSecretName}' is undefined` + ); + await assert.rejects( + () => ssmSecretClient.getSecret('', { name: testSecretName }), + expectedErr + ); + }); + void it('throws error', async () => { const ssmNotFoundException = new ParameterNotFound({ $metadata: {}, @@ -105,7 +124,6 @@ void describe('SSMSecret', () => { mock.method(ssmClient, 'getParameter', () => Promise.reject(ssmNotFoundException) ); - const ssmSecretClient = new SSMSecretClient(ssmClient); const expectedErr = SecretError.fromSSMException(ssmNotFoundException); await assert.rejects( () => ssmSecretClient.getSecret('', { name: '' }), diff --git a/packages/backend-secret/src/ssm_secret.ts b/packages/backend-secret/src/ssm_secret.ts index 78aad4f112..10c875be74 100644 --- a/packages/backend-secret/src/ssm_secret.ts +++ b/packages/backend-secret/src/ssm_secret.ts @@ -87,7 +87,8 @@ export class SSMSecretClient implements SecretClient { public getSecret = async ( backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier - ): Promise => { + ): Promise => { + let secret: Secret | undefined; const name = this.getParameterFullPath( backendIdentifier, secretIdentifier.name @@ -96,22 +97,27 @@ export class SSMSecretClient implements SecretClient { const resp = await this.ssmClient.getParameter({ Name: secretIdentifier.version ? `${name}:${secretIdentifier.version}` - : `${name}`, + : name, WithDecryption: true, }); - if (resp.Parameter?.Name && resp.Parameter?.Value) { - return { - secretIdentifier: { - name: resp.Parameter?.Name, - version: resp.Parameter?.Version, - }, + if (resp.Parameter?.Value) { + secret = { + name: secretIdentifier.name, + version: resp.Parameter?.Version, value: resp.Parameter?.Value, }; } - return; } catch (err) { throw SecretError.fromSSMException(err as SSMServiceException); } + + if (!secret) { + throw new SecretError( + `The value of secret '${secretIdentifier.name}' is undefined` + ); + } + + return secret; }; /** diff --git a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts index 08023595f2..9681b6f10f 100644 --- a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts +++ b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts @@ -25,7 +25,7 @@ const testSecretId: SecretIdentifier = { }; const testSecret: Secret = { - secretIdentifier: testSecretId, + ...testSecretId, value: testSecretValue, }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 06a1b1b6d0..13f9f6fe63 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://github.com/aws-amplify/cli#readme", "dependencies": { + "@aws-amplify/backend-secret": "^0.2.0-alpha.0", "@aws-amplify/client-config": "^0.2.0-alpha.6", "@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0", "@aws-amplify/model-generator": "^0.2.0-alpha.2", diff --git a/packages/cli/src/commands/printer/.eslintrc.json b/packages/cli/src/commands/printer/.eslintrc.json new file mode 100644 index 0000000000..d5ba8f9d9c --- /dev/null +++ b/packages/cli/src/commands/printer/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "off" + } +} diff --git a/packages/cli/src/commands/printer/printer.ts b/packages/cli/src/commands/printer/printer.ts new file mode 100644 index 0000000000..8c1ba8293c --- /dev/null +++ b/packages/cli/src/commands/printer/printer.ts @@ -0,0 +1,31 @@ +import { EOL } from 'os'; + +/** + * The class that pretty prints to the console. + */ +export class Printer { + /** + * Print an object/record to console. + */ + static printRecord = >( + object: T + ): void => { + let message = ''; + const entries = Object.entries(object); + entries.forEach(([key, val]) => { + message = message.concat(` ${key}: ${val}${EOL}`); + }); + console.log(message); + }; + + /** + * Prints an array of objects/records to console. + */ + static printRecords = >( + objects: T[] + ): void => { + for (const obj of objects) { + this.printRecord(obj); + } + }; +} diff --git a/packages/cli/src/commands/prompter/amplify_prompts.ts b/packages/cli/src/commands/prompter/amplify_prompts.ts index 3432d5deda..07ec2e597e 100644 --- a/packages/cli/src/commands/prompter/amplify_prompts.ts +++ b/packages/cli/src/commands/prompter/amplify_prompts.ts @@ -1,4 +1,4 @@ -import { confirm } from '@inquirer/prompts'; +import { confirm, password } from '@inquirer/prompts'; /** * Wrapper for prompter library @@ -22,4 +22,17 @@ export class AmplifyPrompter { }); return response; }; + + /** + * A secret prompt. + */ + static secretValue = async ( + promptMessage = 'Enter secret value' + ): Promise => { + return await password({ + message: promptMessage, + validate: (val: string) => + val && val.length > 0 ? true : 'Cannot be empty', + }); + }; } diff --git a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts index d017cf965b..42c1db9f61 100644 --- a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts @@ -6,6 +6,7 @@ import assert from 'node:assert'; import { SandboxDeleteCommand } from './sandbox_delete_command.js'; import { SandboxCommand } from '../sandbox_command.js'; import { SandboxSingletonFactory } from '@aws-amplify/sandbox'; +import { createSandboxSecretCommand } from '../sandbox-secret/sandbox_secret_command_factory.js'; void describe('sandbox delete command', () => { let commandRunner: TestCommandRunner; @@ -21,11 +22,10 @@ void describe('sandbox delete command', () => { ) as never; // couldn't figure out a good way to type the sandboxDeleteMock so that TS was happy here const sandboxDeleteCommand = new SandboxDeleteCommand(sandboxFactory); - - const sandboxCommand = new SandboxCommand( - sandboxFactory, - sandboxDeleteCommand - ); + const sandboxCommand = new SandboxCommand(sandboxFactory, [ + sandboxDeleteCommand, + createSandboxSecretCommand(), + ]); const parser = yargs().command(sandboxCommand as unknown as CommandModule); commandRunner = new TestCommandRunner(parser); sandboxDeleteMock.mock.resetCalls(); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/constants.ts b/packages/cli/src/commands/sandbox/sandbox-secret/constants.ts new file mode 100644 index 0000000000..feed1ac615 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/constants.ts @@ -0,0 +1 @@ +export const SANDBOX_BRANCH = 'sandbox'; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.test.ts new file mode 100644 index 0000000000..f2ef33d733 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.test.ts @@ -0,0 +1,52 @@ +import { describe, it } from 'node:test'; +import yargs, { CommandModule } from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { getSecretClient } from '@aws-amplify/backend-secret'; +import { SandboxSecretCommand } from './sandbox_secret_command.js'; +import { SandboxSecretGetCommand } from './sandbox_secret_get_command.js'; + +const testBackendId = 'testBackendId'; + +void describe('sandbox secret command', () => { + const secretClient = getSecretClient(); + const sandboxIdResolver = new SandboxIdResolver({ + resolve: () => Promise.resolve(testBackendId), + }); + + // Creates only a 'get' subcommand. + const sandboxSecretCmd = new SandboxSecretCommand([ + new SandboxSecretGetCommand( + sandboxIdResolver, + secretClient + ) as unknown as CommandModule, + ]); + + const parser = yargs().command(sandboxSecretCmd); + const commandRunner = new TestCommandRunner(parser); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('secret --help'); + assert.match(output, /Manage sandbox secret/); + ['secret get'].forEach((cmd) => assert.match(output, new RegExp(cmd))); + ['secret set', 'secret list', 'secret remove'].forEach((cmd) => + assert.doesNotMatch(output, new RegExp(cmd)) + ); + }); + + void it('throws error if no verb subcommand', async () => { + await assert.rejects( + () => commandRunner.runCommand('secret'), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Not enough non-option arguments/); + assert.match(err.output, /Not enough non-option arguments/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.ts new file mode 100644 index 0000000000..a6568b243d --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command.ts @@ -0,0 +1,56 @@ +import { Argv, CommandModule } from 'yargs'; + +/** + * Root command to manage sandbox secret. + */ +export class SandboxSecretCommand implements CommandModule { + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + /** + * Root command to manage sandbox secret + */ + constructor(private readonly secretSubCommands: CommandModule[]) { + this.command = 'secret '; + this.describe = 'Manage sandbox secret'; + } + + /** + * @inheritDoc + */ + handler = (): void => { + // no-op for non-terminal command. + return; + }; + + /** + * @inheritDoc + */ + builder = (yargs: Argv): Argv => { + return ( + yargs + .command(this.secretSubCommands) + // Hide inherited options since they are not applicable here. + .option('dirToWatch', { + hidden: true, + }) + .option('exclude', { + hidden: true, + }) + .option('name', { + hidden: true, + }) + .option('out', { + hidden: true, + }) + .help() + ); + }; +} diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.test.ts new file mode 100644 index 0000000000..9a7183abd4 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from 'node:test'; +import yargs from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { createSandboxSecretCommand } from './sandbox_secret_command_factory.js'; + +void describe('sandbox secret command factory', () => { + const sandboxSecretCmd = createSandboxSecretCommand(); + + const parser = yargs().command(sandboxSecretCmd); + const commandRunner = new TestCommandRunner(parser); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('secret --help'); + assert.match(output, /Manage sandbox secret/); + ['secret set', 'secret remove', 'secret get ', 'secret list'].forEach( + (cmd) => assert.match(output, new RegExp(cmd)) + ); + }); + + void it('throws error if no verb subcommand', async () => { + await assert.rejects( + () => commandRunner.runCommand('secret'), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Not enough non-option arguments/); + assert.match(err.output, /Not enough non-option arguments/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.ts new file mode 100644 index 0000000000..f3af92b52f --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_command_factory.ts @@ -0,0 +1,45 @@ +import { CommandModule } from 'yargs'; + +import { LocalAppNameResolver } from '../../../backend-identifier/local_app_name_resolver.js'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { CwdPackageJsonLoader } from '../../../cwd_package_json_loader.js'; +import { SandboxSecretCommand } from './sandbox_secret_command.js'; +import { getSecretClient } from '@aws-amplify/backend-secret'; +import { SandboxSecretSetCommand } from './sandbox_secret_set_command.js'; +import { SandboxSecretRemoveCommand } from './sandbox_secret_remove_command.js'; +import { SandboxSecretGetCommand } from './sandbox_secret_get_command.js'; +import { SandboxSecretListCommand } from './sandbox_secret_list_command.js'; + +/** + * Creates sandbox secret commands. + */ +export const createSandboxSecretCommand = (): CommandModule => { + const sandboxIdResolver = new SandboxIdResolver( + new LocalAppNameResolver(new CwdPackageJsonLoader()) + ); + + const secretClient = getSecretClient(); + const setCommand = new SandboxSecretSetCommand( + sandboxIdResolver, + secretClient + ); + const removeCommand = new SandboxSecretRemoveCommand( + sandboxIdResolver, + secretClient + ); + const getCommand = new SandboxSecretGetCommand( + sandboxIdResolver, + secretClient + ); + const listCommand = new SandboxSecretListCommand( + sandboxIdResolver, + secretClient + ); + + return new SandboxSecretCommand([ + setCommand as unknown as CommandModule, + removeCommand as unknown as CommandModule, + getCommand as unknown as CommandModule, + listCommand, + ]); +}; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts new file mode 100644 index 0000000000..eb4d7970cd --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import yargs, { CommandModule } from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { + Secret, + SecretIdentifier, + getSecretClient, +} from '@aws-amplify/backend-secret'; +import { SandboxSecretGetCommand } from './sandbox_secret_get_command.js'; +import { SANDBOX_BRANCH } from './constants.js'; +import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types'; +import { Printer } from '../../printer/printer.js'; + +const testSecretName = 'testSecretName'; +const testBackendId = 'testBackendId'; +const testSecretIdentifier: SecretIdentifier = { + name: testSecretName, +}; +const testSecret: Secret = { + ...testSecretIdentifier, + version: 100, + value: 'testValue', +}; + +void describe('sandbox secret get command', () => { + const secretClient = getSecretClient(); + const secretGetMock = mock.method( + secretClient, + 'getSecret', + (): Promise => Promise.resolve(testSecret) + ); + + const sandboxIdResolver = new SandboxIdResolver({ + resolve: () => Promise.resolve(testBackendId), + }); + + const sandboxSecretGetCmd = new SandboxSecretGetCommand( + sandboxIdResolver, + secretClient + ); + + const parser = yargs().command( + sandboxSecretGetCmd as unknown as CommandModule + ); + + const commandRunner = new TestCommandRunner(parser); + + beforeEach(async () => { + secretGetMock.mock.resetCalls(); + }); + + void it('gets a secret', async (contextual) => { + const mockPrintRecord = contextual.mock.method(Printer, 'printRecord'); + + await commandRunner.runCommand(`get ${testSecretName}`); + + assert.equal(secretGetMock.mock.callCount(), 1); + const backendIdentifier = secretGetMock.mock.calls[0] + .arguments[0] as UniqueBackendIdentifier; + assert.match(backendIdentifier.backendId, new RegExp(testBackendId)); + assert.equal(backendIdentifier.branchName, SANDBOX_BRANCH); + assert.deepStrictEqual( + secretGetMock.mock.calls[0].arguments[1], + testSecretIdentifier + ); + + assert.equal(mockPrintRecord.mock.callCount(), 1); + assert.equal(mockPrintRecord.mock.calls[0].arguments[0], testSecret); + }); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('list --help'); + assert.match(output, /Get a sandbox secret/); + }); + + void it('throws error if no secret name argument', async () => { + await assert.rejects( + () => commandRunner.runCommand('get'), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Not enough non-option arguments/); + assert.match(err.output, /Not enough non-option arguments/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts new file mode 100644 index 0000000000..e913fc4142 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts @@ -0,0 +1,64 @@ +import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; +import { SecretClient } from '@aws-amplify/backend-secret'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SANDBOX_BRANCH } from './constants.js'; +import { Printer } from '../../printer/printer.js'; + +/** + * Command to get sandbox secret. + */ +export class SandboxSecretGetCommand + implements CommandModule +{ + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + /** + * Get sandbox secret command. + */ + constructor( + private readonly sandboxIdResolver: SandboxIdResolver, + private readonly secretClient: SecretClient + ) { + this.command = 'get '; + this.describe = 'Get a sandbox secret'; + } + + /** + * @inheritDoc + */ + handler = async ( + args: ArgumentsCamelCase + ): Promise => { + const backendId = await this.sandboxIdResolver.resolve(); + const secret = await this.secretClient.getSecret( + { backendId, branchName: SANDBOX_BRANCH }, + { name: args.secretName } + ); + Printer.printRecord(secret); + }; + + /** + * @inheritDoc + */ + builder = (yargs: Argv): Argv => { + return yargs + .positional('secretName', { + describe: 'Name of the secret to get', + type: 'string', + demandOption: true, + }) + .help(); + }; +} + +type SecretGetCommandOptions = { + secretName: string; +}; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts new file mode 100644 index 0000000000..8a20b16277 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import yargs from 'yargs'; +import { TestCommandRunner } from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SecretIdentifier, getSecretClient } from '@aws-amplify/backend-secret'; +import { SANDBOX_BRANCH } from './constants.js'; +import { SandboxSecretListCommand } from './sandbox_secret_list_command.js'; +import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types'; +import { Printer } from '../../printer/printer.js'; + +const testBackendId = 'testBackendId'; + +const testSecretIds: SecretIdentifier[] = [ + { + name: 'testSecret1', + version: 12, + }, + { + name: 'testSecret2', + version: 24, + }, +]; + +void describe('sandbox secret list command', () => { + const secretClient = getSecretClient(); + const secretListMock = mock.method( + secretClient, + 'listSecrets', + (): Promise => + Promise.resolve(testSecretIds) + ); + const sandboxIdResolver = new SandboxIdResolver({ + resolve: () => Promise.resolve(testBackendId), + }); + + const sandboxSecretListCmd = new SandboxSecretListCommand( + sandboxIdResolver, + secretClient + ); + + const parser = yargs().command(sandboxSecretListCmd); + const commandRunner = new TestCommandRunner(parser); + + beforeEach(async () => { + secretListMock.mock.resetCalls(); + }); + + void it('list secrets', async (contextual) => { + const mockPrintRecords = contextual.mock.method(Printer, 'printRecords'); + + await commandRunner.runCommand(`list`); + assert.equal(secretListMock.mock.callCount(), 1); + + const backendIdentifier = secretListMock.mock.calls[0] + .arguments[0] as UniqueBackendIdentifier; + assert.match(backendIdentifier.backendId, new RegExp(testBackendId)); + assert.equal(backendIdentifier.branchName, SANDBOX_BRANCH); + + assert.equal(mockPrintRecords.mock.callCount(), 1); + assert.equal(mockPrintRecords.mock.calls[0].arguments[0], testSecretIds); + }); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('list --help'); + assert.match(output, /List all sandbox secrets/); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts new file mode 100644 index 0000000000..fd6ea947ef --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts @@ -0,0 +1,43 @@ +import { CommandModule } from 'yargs'; +import { SecretClient } from '@aws-amplify/backend-secret'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SANDBOX_BRANCH } from './constants.js'; +import { Printer } from '../../printer/printer.js'; + +/** + * Command to list sandbox secrets. + */ +export class SandboxSecretListCommand implements CommandModule { + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + /** + * List sandbox secret command. + */ + constructor( + private readonly sandboxIdResolver: SandboxIdResolver, + private readonly secretClient: SecretClient + ) { + this.command = 'list'; + this.describe = 'List all sandbox secrets'; + } + + /** + * @inheritDoc + */ + handler = async (): Promise => { + const backendId = await this.sandboxIdResolver.resolve(); + const secretIds = await this.secretClient.listSecrets({ + backendId, + branchName: SANDBOX_BRANCH, + }); + Printer.printRecords(secretIds); + }; +} diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.test.ts new file mode 100644 index 0000000000..429841ba5a --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import yargs, { CommandModule } from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { getSecretClient } from '@aws-amplify/backend-secret'; +import { SandboxSecretRemoveCommand } from './sandbox_secret_remove_command.js'; +import { SANDBOX_BRANCH } from './constants.js'; +import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types'; + +const testSecretName = 'testSecretName'; +const testBackendId = 'testBackendId'; + +void describe('sandbox secret remove command', () => { + const secretClient = getSecretClient(); + const secretRemoveMock = mock.method( + secretClient, + 'removeSecret', + (): Promise => Promise.resolve() + ); + + const sandboxIdResolver = new SandboxIdResolver({ + resolve: () => Promise.resolve('testBackendId'), + }); + + const sandboxSecretRemoveCmd = new SandboxSecretRemoveCommand( + sandboxIdResolver, + secretClient + ); + + const parser = yargs().command( + sandboxSecretRemoveCmd as unknown as CommandModule + ); + + const commandRunner = new TestCommandRunner(parser); + + beforeEach(async () => { + secretRemoveMock.mock.resetCalls(); + }); + + void it('remove a secret', async () => { + await commandRunner.runCommand(`remove ${testSecretName}`); + assert.equal(secretRemoveMock.mock.callCount(), 1); + const backendIdentifier = secretRemoveMock.mock.calls[0] + .arguments[0] as UniqueBackendIdentifier; + assert.match(backendIdentifier.backendId, new RegExp(testBackendId)); + assert.equal(backendIdentifier.branchName, SANDBOX_BRANCH); + assert.equal(secretRemoveMock.mock.calls[0].arguments[1], testSecretName); + }); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('remove --help'); + assert.match(output, /Remove a sandbox secret/); + }); + + void it('throws error if no secret name argument', async () => { + await assert.rejects( + () => commandRunner.runCommand('remove'), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Not enough non-option arguments/); + assert.match(err.output, /Not enough non-option arguments/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.ts new file mode 100644 index 0000000000..5922820e18 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_remove_command.ts @@ -0,0 +1,63 @@ +import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; +import { SecretClient } from '@aws-amplify/backend-secret'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SANDBOX_BRANCH } from './constants.js'; + +/** + * Command to remove sandbox secret. + */ +export class SandboxSecretRemoveCommand + implements CommandModule +{ + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + /** + * Remove sandbox secret command. + */ + constructor( + private readonly sandboxIdResolver: SandboxIdResolver, + private readonly secretClient: SecretClient + ) { + this.command = 'remove '; + this.describe = 'Remove a sandbox secret'; + } + + /** + * @inheritDoc + */ + handler = async ( + args: ArgumentsCamelCase + ): Promise => { + const backendId = await this.sandboxIdResolver.resolve(); + await this.secretClient.removeSecret( + { + backendId, + branchName: SANDBOX_BRANCH, + }, + args.secretName + ); + }; + + /** + * @inheritDoc + */ + builder = (yargs: Argv): Argv => { + return yargs.positional('secretName', { + describe: 'Name of the secret to remove', + type: 'string', + demandOption: true, + }); + }; +} + +type SecretRemoveCommandOptions = { + secretName: string; +}; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts new file mode 100644 index 0000000000..ad3d112ed7 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { AmplifyPrompter } from '../../prompter/amplify_prompts.js'; +import yargs, { CommandModule } from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SecretIdentifier, getSecretClient } from '@aws-amplify/backend-secret'; +import { SANDBOX_BRANCH } from './constants.js'; +import { SandboxSecretSetCommand } from './sandbox_secret_set_command.js'; +import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types'; +import { Printer } from '../../printer/printer.js'; + +const testSecretName = 'testSecretName'; +const testSecretValue = 'testSecretValue'; +const testSecretIdentifier: SecretIdentifier = { + name: testSecretName, + version: 100, +}; + +const testBackendId = 'testBackendId'; + +void describe('sandbox secret set command', () => { + const secretClient = getSecretClient(); + const secretSetMock = mock.method( + secretClient, + 'setSecret', + (): Promise => Promise.resolve(testSecretIdentifier) + ); + + const sandboxIdResolver = new SandboxIdResolver({ + resolve: () => Promise.resolve(testBackendId), + }); + + const sandboxSecretSetCmd = new SandboxSecretSetCommand( + sandboxIdResolver, + secretClient + ); + + const parser = yargs().command( + sandboxSecretSetCmd as unknown as CommandModule + ); + + const commandRunner = new TestCommandRunner(parser); + + beforeEach(async () => { + secretSetMock.mock.resetCalls(); + }); + + void it('sets a secret', async (contextual) => { + const mockSecretValue = contextual.mock.method( + AmplifyPrompter, + 'secretValue', + () => Promise.resolve(testSecretValue) + ); + + const mockPrintRecord = contextual.mock.method(Printer, 'printRecord'); + + await commandRunner.runCommand(`set ${testSecretName}`); + assert.equal(mockSecretValue.mock.callCount(), 1); + assert.equal(mockPrintRecord.mock.callCount(), 1); + assert.deepStrictEqual( + mockPrintRecord.mock.calls[0].arguments[0], + testSecretIdentifier + ); + assert.equal(secretSetMock.mock.callCount(), 1); + + const backendIdentifier = secretSetMock.mock.calls[0] + .arguments[0] as UniqueBackendIdentifier; + assert.match(backendIdentifier.backendId, new RegExp(testBackendId)); + assert.equal(backendIdentifier.branchName, SANDBOX_BRANCH); + assert.equal(secretSetMock.mock.calls[0].arguments[1], testSecretName); + assert.equal(secretSetMock.mock.calls[0].arguments[2], testSecretValue); + }); + + void it('show --help', async () => { + const output = await commandRunner.runCommand('set --help'); + assert.match(output, /Set a sandbox secret/); + }); + + void it('throws error if no secret name argument', async () => { + await assert.rejects( + () => commandRunner.runCommand('set'), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Not enough non-option arguments/); + assert.match(err.output, /Not enough non-option arguments/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts new file mode 100644 index 0000000000..583c2d6769 --- /dev/null +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts @@ -0,0 +1,65 @@ +import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; +import { SecretClient } from '@aws-amplify/backend-secret'; +import { SandboxIdResolver } from '../sandbox_id_resolver.js'; +import { SANDBOX_BRANCH } from './constants.js'; +import { AmplifyPrompter } from '../../prompter/amplify_prompts.js'; +import { Printer } from '../../printer/printer.js'; + +/** + * Command to set sandbox secret. + */ +export class SandboxSecretSetCommand + implements CommandModule +{ + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + /** + * Set sandbox secret command. + */ + constructor( + private readonly sandboxIdResolver: SandboxIdResolver, + private readonly secretClient: SecretClient + ) { + this.command = 'set '; + this.describe = 'Set a sandbox secret'; + } + + /** + * @inheritDoc + */ + handler = async ( + args: ArgumentsCamelCase + ): Promise => { + const secretVal = await AmplifyPrompter.secretValue(); + const backendId = await this.sandboxIdResolver.resolve(); + const secretId = await this.secretClient.setSecret( + { backendId, branchName: SANDBOX_BRANCH }, + args.secretName, + secretVal + ); + Printer.printRecord(secretId); + }; + + /** + * @inheritDoc + */ + builder = (yargs: Argv): Argv => { + return yargs.positional('secretName', { + describe: 'Name of the secret to set', + type: 'string', + demandOption: true, + }); + }; +} + +type SecretSetCommandOptions = { + secretName: string; +}; diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 7cd3a57f85..62d5c93da7 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -11,6 +11,7 @@ import { SandboxCommand } from './sandbox_command.js'; import { createSandboxCommand } from './sandbox_command_factory.js'; import { SandboxDeleteCommand } from './sandbox-delete/sandbox_delete_command.js'; import { Sandbox, SandboxSingletonFactory } from '@aws-amplify/sandbox'; +import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js'; void describe('sandbox command factory', () => { void it('instantiate a sandbox command correctly', () => { @@ -31,11 +32,10 @@ void describe('sandbox command', () => { sandboxStartMock = mock.method(sandbox, 'start', () => Promise.resolve()); const sandboxDeleteCommand = new SandboxDeleteCommand(sandboxFactory); - - const sandboxCommand = new SandboxCommand( - sandboxFactory, - sandboxDeleteCommand - ); + const sandboxCommand = new SandboxCommand(sandboxFactory, [ + sandboxDeleteCommand, + createSandboxSecretCommand(), + ]); const parser = yargs().command(sandboxCommand as unknown as CommandModule); commandRunner = new TestCommandRunner(parser); sandboxStartMock.mock.resetCalls(); diff --git a/packages/cli/src/commands/sandbox/sandbox_command.ts b/packages/cli/src/commands/sandbox/sandbox_command.ts index f2dae5010f..64a2e2cba8 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.ts @@ -1,6 +1,5 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { ClientConfigFormat } from '@aws-amplify/client-config'; -import { SandboxDeleteCommand } from './sandbox-delete/sandbox_delete_command.js'; import fs from 'fs'; import { AmplifyPrompter } from '../prompter/amplify_prompts.js'; import { SandboxSingletonFactory } from '@aws-amplify/sandbox'; @@ -37,7 +36,7 @@ export class SandboxCommand */ constructor( private readonly sandboxFactory: SandboxSingletonFactory, - private readonly sandboxDeleteCommand: SandboxDeleteCommand + private readonly sandboxSubCommands: CommandModule[] ) { this.command = 'sandbox'; this.describe = 'Starts sandbox, watch mode for amplify deployments'; @@ -70,7 +69,7 @@ export class SandboxCommand return ( yargs // Cast to erase options types used in internal sub command implementation. Otherwise, compiler fails here. - .command(this.sandboxDeleteCommand as unknown as CommandModule) + .command(this.sandboxSubCommands) .option('dirToWatch', { describe: 'Directory to watch for file changes. All subdirectories and files will be included. defaults to the current directory.', diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index 5101f075a6..56b0681c00 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -6,6 +6,7 @@ import { SandboxDeleteCommand } from './sandbox-delete/sandbox_delete_command.js import { SandboxIdResolver } from './sandbox_id_resolver.js'; import { CwdPackageJsonLoader } from '../../cwd_package_json_loader.js'; import { LocalAppNameResolver } from '../../backend-identifier/local_app_name_resolver.js'; +import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js'; /** * Creates wired sandbox command. @@ -18,8 +19,9 @@ export const createSandboxCommand = (): CommandModule< new LocalAppNameResolver(new CwdPackageJsonLoader()) ); const sandboxFactory = new SandboxSingletonFactory(sandboxIdResolver.resolve); - return new SandboxCommand( - sandboxFactory, - new SandboxDeleteCommand(sandboxFactory) - ); + + return new SandboxCommand(sandboxFactory, [ + new SandboxDeleteCommand(sandboxFactory), + createSandboxSecretCommand(), + ]); }; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ea291bde69..79664010ed 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "lib" }, "references": [ + { "path": "../backend-secret" }, { "path": "../client-config" }, { "path": "../deployed-backend-client" }, { "path": "../model-generator" },