diff --git a/.changeset/beige-dragons-boil.md b/.changeset/beige-dragons-boil.md new file mode 100644 index 0000000000..bc41e3d059 --- /dev/null +++ b/.changeset/beige-dragons-boil.md @@ -0,0 +1,8 @@ +--- +'@aws-amplify/integration-tests': patch +'create-amplify': patch +'@aws-amplify/client-config': patch +'@aws-amplify/backend-cli': patch +--- + +chore: rename the new client config file name diff --git a/.changeset/modern-fans-return.md b/.changeset/modern-fans-return.md new file mode 100644 index 0000000000..46f352bd90 --- /dev/null +++ b/.changeset/modern-fans-return.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-cli': patch +--- + +Added normalization logic for AWS environment variables diff --git a/.changeset/silent-rules-obey.md b/.changeset/silent-rules-obey.md new file mode 100644 index 0000000000..d04eaa5be0 --- /dev/null +++ b/.changeset/silent-rules-obey.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/integration-tests': patch +'create-amplify': patch +--- + +fix: order of array in test assert in gitignored tests diff --git a/.changeset/tough-bananas-mix.md b/.changeset/tough-bananas-mix.md new file mode 100644 index 0000000000..b09e5abdc9 --- /dev/null +++ b/.changeset/tough-bananas-mix.md @@ -0,0 +1,7 @@ +--- +'@aws-amplify/cli-core': patch +'@aws-amplify/sandbox': patch +'@aws-amplify/backend-cli': patch +--- + +chore: Adds a log message to inform the name of the sandbox being created/initialized diff --git a/packages/cli-core/API.md b/packages/cli-core/API.md index 9243780496..109ab5d54e 100644 --- a/packages/cli-core/API.md +++ b/packages/cli-core/API.md @@ -29,8 +29,11 @@ export const format: { error: (message: string) => string; note: (message: string) => string; command: (command: string) => string; + highlight: (command: string) => string; success: (message: string) => string; sectionHeader: (header: string) => string; + bold: (message: string) => string; + dim: (message: string) => string; link: (link: string) => string; list: (lines: string[]) => string; indent: (message: string) => string; diff --git a/packages/cli-core/src/format/format.ts b/packages/cli-core/src/format/format.ts index c65cfef227..a81a76cfb3 100644 --- a/packages/cli-core/src/format/format.ts +++ b/packages/cli-core/src/format/format.ts @@ -1,5 +1,14 @@ import * as os from 'node:os'; -import { blue, bold, cyan, green, grey, red, underline } from 'kleur/colors'; +import { + blue, + bold, + cyan, + dim, + green, + grey, + red, + underline, +} from 'kleur/colors'; /** * Formats various inputs into single string. @@ -16,8 +25,11 @@ export const format = { error: (message: string) => red(message), note: (message: string) => grey(message), command: (command: string) => cyan(command), + highlight: (command: string) => cyan(command), success: (message: string) => green(message), sectionHeader: (header: string) => bold(blue(header)), + bold: (message: string) => bold(message), + dim: (message: string) => dim(message), link: (link: string) => underline(blue(link)), list: (lines: string[]) => lines.map((line: string) => ` - ${line}`).join(os.EOL), diff --git a/packages/cli/src/command_middleware.test.ts b/packages/cli/src/command_middleware.test.ts index abb0296e9a..313b4a2003 100644 --- a/packages/cli/src/command_middleware.test.ts +++ b/packages/cli/src/command_middleware.test.ts @@ -1,11 +1,12 @@ import assert from 'node:assert'; -import { after, before, beforeEach, describe, it } from 'node:test'; +import { after, before, beforeEach, describe, it, mock } from 'node:test'; import { CommandMiddleware } from './command_middleware.js'; import { EOL } from 'node:os'; import { DEFAULT_PROFILE } from '@smithy/shared-ini-file-loader'; import fs from 'fs/promises'; import path from 'path'; import { ArgumentsCamelCase } from 'yargs'; +import { Printer } from '@aws-amplify/cli-core'; const restoreEnv = (restoreVal: string | undefined, envVar: string) => { if (restoreVal) { @@ -17,7 +18,10 @@ const restoreEnv = (restoreVal: string | undefined, envVar: string) => { void describe('commandMiddleware', () => { void describe('ensureAwsCredentialAndRegion', () => { - const commandMiddleware = new CommandMiddleware(); + const printerMock = { log: mock.fn() }; + const commandMiddleware = new CommandMiddleware( + printerMock as unknown as Printer + ); const testAccessKeyId = '124'; const testSecretAccessKey = '667'; const testProfile = 'profileA'; @@ -28,11 +32,13 @@ void describe('commandMiddleware', () => { let configFilePath: string; const currentProfile = process.env.AWS_PROFILE; + const currentDefaultProfile = process.env.AWS_DEFAULT_PROFILE; const currentConfigFile = process.env.AWS_CONFIG_FILE; const currentCredentialFile = process.env.AWS_SHARED_CREDENTIALS_FILE; const currentAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const currentSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const currentRegion = process.env.AWS_REGION; + const currentDefaultRegion = process.env.AWS_DEFAULT_REGION; before(async () => { testDir = await fs.mkdtemp('profile_middleware_test'); @@ -45,11 +51,13 @@ void describe('commandMiddleware', () => { after(async () => { await fs.rm(testDir, { recursive: true, force: true }); restoreEnv(currentProfile, 'AWS_PROFILE'); + restoreEnv(currentDefaultProfile, 'AWS_DEFAULT_PROFILE'); restoreEnv(currentConfigFile, 'AWS_CONFIG_FILE'); restoreEnv(currentCredentialFile, 'AWS_SHARED_CREDENTIALS_FILE'); restoreEnv(currentAccessKeyId, 'AWS_ACCESS_KEY_ID'); restoreEnv(currentSecretAccessKey, 'AWS_SECRET_ACCESS_KEY'); restoreEnv(currentRegion, 'AWS_REGION'); + restoreEnv(currentDefaultRegion, 'AWS_DEFAULT_REGION'); }); void describe('from environment variables', () => { @@ -58,6 +66,7 @@ void describe('commandMiddleware', () => { process.env.AWS_SECRET_ACCESS_KEY = testSecretAccessKey; process.env.AWS_REGION = testRegion; delete process.env.AWS_PROFILE; + printerMock.log.mock.resetCalls(); }); void it('loads credentials', async () => { @@ -68,8 +77,38 @@ void describe('commandMiddleware', () => { ); }); - void it('throws error if absent region environment variable', async () => { + void it('maps AWS_DEFAULT_REGION to AWS_REGION', async () => { delete process.env.AWS_REGION; + process.env.AWS_DEFAULT_REGION = testRegion; + await assert.doesNotReject(() => + commandMiddleware.ensureAwsCredentialAndRegion( + {} as ArgumentsCamelCase<{ profile: string | undefined }> + ) + ); + assert.equal(process.env.AWS_REGION, testRegion); + assert.match( + printerMock.log.mock.calls[0].arguments[0], + /Legacy environment variable/ + ); + }); + + void it('prefers AWS_REGION to AWS_DEFAULT_REGION', async () => { + process.env.AWS_DEFAULT_REGION = 'testDefaultRegion'; + await assert.doesNotReject(() => + commandMiddleware.ensureAwsCredentialAndRegion( + {} as ArgumentsCamelCase<{ profile: string | undefined }> + ) + ); + assert.equal(process.env.AWS_REGION, testRegion); + assert.match( + printerMock.log.mock.calls[0].arguments[0], + /Using 'AWS_REGION'/ + ); + }); + + void it('throws error if absent region environment variables', async () => { + delete process.env.AWS_REGION; + delete process.env.AWS_DEFAULT_REGION; try { await commandMiddleware.ensureAwsCredentialAndRegion( {} as ArgumentsCamelCase<{ profile: string | undefined }> @@ -110,6 +149,7 @@ void describe('commandMiddleware', () => { await fs.writeFile(credFilePath, '', 'utf-8'); await fs.writeFile(configFilePath, '', 'utf-8'); delete process.env.AWS_PROFILE; + printerMock.log.mock.resetCalls(); }); const writeProfileCredential = async ( @@ -208,6 +248,41 @@ void describe('commandMiddleware', () => { } as ArgumentsCamelCase<{ profile: string | undefined }>) ); }); + + void it('maps AWS_DEFAULT_PROFILE to AWS_PROFILE', async () => { + process.env.AWS_DEFAULT_PROFILE = testProfile; + await writeProfileRegion(testProfile); + await writeProfileCredential(testProfile); + + await assert.doesNotReject(() => + commandMiddleware.ensureAwsCredentialAndRegion( + {} as ArgumentsCamelCase<{ profile: string | undefined }> + ) + ); + assert.equal(process.env.AWS_PROFILE, testProfile); + assert.match( + printerMock.log.mock.calls[0].arguments[0], + /Legacy environment variable/ + ); + }); + + void it('prefers AWS_PROFILE over AWS_DEFAULT_PROFILE', async () => { + process.env.AWS_DEFAULT_PROFILE = 'testDefaultProfile'; + process.env.AWS_PROFILE = testProfile; + await writeProfileRegion(testProfile); + await writeProfileCredential(testProfile); + + await assert.doesNotReject(() => + commandMiddleware.ensureAwsCredentialAndRegion( + {} as ArgumentsCamelCase<{ profile: string | undefined }> + ) + ); + assert.equal(process.env.AWS_PROFILE, testProfile); + assert.match( + printerMock.log.mock.calls[0].arguments[0], + /Using 'AWS_PROFILE'/ + ); + }); }); }); }); diff --git a/packages/cli/src/command_middleware.ts b/packages/cli/src/command_middleware.ts index 4d14b280e9..ee49cb3c8b 100644 --- a/packages/cli/src/command_middleware.ts +++ b/packages/cli/src/command_middleware.ts @@ -3,6 +3,7 @@ import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { loadConfig } from '@smithy/node-config-provider'; import { NODE_REGION_CONFIG_OPTIONS } from '@aws-sdk/region-config-resolver'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { Printer } from '@aws-amplify/cli-core'; export const profileSetupInstruction = `To configure a new Amplify profile, use "npx amplify configure profile".`; @@ -10,6 +11,11 @@ export const profileSetupInstruction = `To configure a new Amplify profile, use * Contains middleware functions. */ export class CommandMiddleware { + /** + * Creates command middleware. + */ + constructor(private readonly printer: Printer) {} + /** * Ensure AWS credentials and region of the input profile (or 'default' if undefined) are available in the provider chain. * If the input profile is defined, the environment variable AWS_PROFILE will be set accordingly. @@ -19,6 +25,15 @@ export class CommandMiddleware { >( argv: ArgumentsCamelCase ) => { + /** + * The AWS CDK respects older CLI v1 variable names that are no longer supported in the + * latest AWS SDK. Developers that use the older variables and switch between Amplify + * and CDK tools will experience region mismatch failures when using Amplify tools. Variable + * names known to cause such failures are mapped here for a better developer experience. + */ + this.mapEnvironmentVariables('AWS_DEFAULT_REGION', 'AWS_REGION'); + this.mapEnvironmentVariables('AWS_DEFAULT_PROFILE', 'AWS_PROFILE'); + if (argv.profile) { process.env.AWS_PROFILE = argv.profile; } @@ -61,4 +76,26 @@ export class CommandMiddleware { ); } }; + + /** + * Maps one environment variable name to the other + */ + private mapEnvironmentVariables( + legacyName: string, + preferredName: string + ): void { + if (!process.env[legacyName]) { + return; + } + if (process.env[preferredName]) { + this.printer.log( + `Both the legacy '${legacyName}' and preferred '${preferredName}' environment variables detected. Using '${preferredName}'` + ); + return; + } + this.printer.log( + `Legacy environment variable '${legacyName}' detected. Mapping to '${preferredName}'` + ); + process.env[preferredName] = process.env[legacyName]; + } } diff --git a/packages/cli/src/commands/generate/config/generate_config_command.ts b/packages/cli/src/commands/generate/config/generate_config_command.ts index 89b81ed851..21af80d489 100644 --- a/packages/cli/src/commands/generate/config/generate_config_command.ts +++ b/packages/cli/src/commands/generate/config/generate_config_command.ts @@ -111,7 +111,7 @@ export class GenerateConfigCommand }) .option('config-version', { describe: - 'Version of the client config. Version 0 represents classic amplify-cli client config amplify-configuration (Default) and 1 represents new unified client config amplify-outputs', + 'Version of the client config. Version 0 represents classic amplify-cli client config amplify-configuration (Default) and 1 represents new unified client config amplify_outputs', type: 'string', array: false, choices: Object.values(ClientConfigVersionOption), diff --git a/packages/cli/src/commands/generate/generate_command_factory.ts b/packages/cli/src/commands/generate/generate_command_factory.ts index 2d58eb0f7d..4a150debe9 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.ts @@ -17,6 +17,7 @@ import { AppBackendIdentifierResolver } from '../../backend-identifier/backend_i import { GenerateSchemaCommand } from './schema-from-database/generate_schema_command.js'; import { getSecretClient } from '@aws-amplify/backend-secret'; import { SchemaGenerator } from '@aws-amplify/schema-generator'; +import { printer } from '@aws-amplify/cli-core'; /** * Creates wired generate command. @@ -63,7 +64,7 @@ export const createGenerateCommand = (): CommandModule => { new SchemaGenerator() ); - const commandMiddleware = new CommandMiddleware(); + const commandMiddleware = new CommandMiddleware(printer); return new GenerateCommand( generateConfigCommand, diff --git a/packages/cli/src/commands/sandbox/option_types.ts b/packages/cli/src/commands/sandbox/option_types.ts index 76fa7ff6bb..0e2e102e48 100644 --- a/packages/cli/src/commands/sandbox/option_types.ts +++ b/packages/cli/src/commands/sandbox/option_types.ts @@ -7,7 +7,7 @@ export type SandboxCommandGlobalOptions = { */ profile?: string; /** - * Optional name to use to distinguish multiple sandboxes + * Optional identifier to use to distinguish multiple sandboxes. */ - name?: string; + identifier?: string; }; 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 fee5a2dbe7..ab11d3cee2 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 @@ -14,7 +14,7 @@ void describe('sandbox delete command', () => { let commandRunner: TestCommandRunner; let sandboxDeleteMock = mock.fn(); - const commandMiddleware = new CommandMiddleware(); + const commandMiddleware = new CommandMiddleware(printer); const mockHandleProfile = mock.method( commandMiddleware, 'ensureAwsCredentialAndRegion', @@ -60,7 +60,7 @@ void describe('sandbox delete command', () => { assert.equal(sandboxDeleteMock.mock.callCount(), 1); assert.deepStrictEqual(sandboxDeleteMock.mock.calls[0].arguments[0], { - name: undefined, + identifier: undefined, }); assert.equal(mockHandleProfile.mock.callCount(), 1); assert.equal( @@ -69,15 +69,15 @@ void describe('sandbox delete command', () => { ); }); - void it('deletes sandbox with user provided name', async (contextual) => { + void it('deletes sandbox with user provided identifier', async (contextual) => { contextual.mock.method(AmplifyPrompter, 'yesOrNo', () => Promise.resolve(true) ); - await commandRunner.runCommand('sandbox delete --name test-App-Name'); + await commandRunner.runCommand('sandbox delete --identifier test-App-Name'); assert.equal(sandboxDeleteMock.mock.callCount(), 1); assert.deepStrictEqual(sandboxDeleteMock.mock.calls[0].arguments[0], { - name: 'test-App-Name', + identifier: 'test-App-Name', }); }); @@ -98,7 +98,7 @@ void describe('sandbox delete command', () => { void it('shows available options in help output', async () => { const output = await commandRunner.runCommand('sandbox delete --help'); assert.match(output, /--yes/); - assert.match(output, /--name/); + assert.match(output, /--identifier/); assert.doesNotMatch(output, /--exclude/); assert.doesNotMatch(output, /--dir-to-watch/); assert.equal(mockHandleProfile.mock.callCount(), 0); diff --git a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.ts b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.ts index 9e15536349..eb4f60a886 100644 --- a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.ts @@ -45,7 +45,7 @@ export class SandboxDeleteCommand if (isConfirmed) { await ( await this.sandboxFactory.getInstance() - ).delete({ name: args.name }); + ).delete({ identifier: args.identifier }); } }; 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 index a273e6984b..01e84bc502 100644 --- 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 @@ -34,10 +34,10 @@ void describe('sandbox secret get command', () => { ); const sandboxIdResolver: SandboxBackendIdResolver = { - resolve: (nameOverride?: string) => + resolve: (identifier?: string) => Promise.resolve({ namespace: testBackendId, - name: nameOverride || testSandboxName, + name: identifier || testSandboxName, type: 'sandbox', }), } as SandboxBackendIdResolver; @@ -79,7 +79,9 @@ void describe('sandbox secret get command', () => { }); void it('gets secret for named sandbox', async () => { - await commandRunner.runCommand(`get ${testSecretName} --name anotherName`); + await commandRunner.runCommand( + `get ${testSecretName} --identifier anotherName` + ); assert.equal(secretGetMock.mock.callCount(), 1); assert.deepStrictEqual(secretGetMock.mock.calls[0].arguments, [ 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 index 476f16da0a..97e981804d 100644 --- 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 @@ -39,7 +39,7 @@ export class SandboxSecretGetCommand args: ArgumentsCamelCase ): Promise => { const sandboxBackendIdentifier = await this.sandboxIdResolver.resolve( - args.name + args.identifier ); const secret = await this.secretClient.getSecret(sandboxBackendIdentifier, { name: args.secretName, 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 index 3fb3ea213c..39a80d5a57 100644 --- 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 @@ -31,10 +31,10 @@ void describe('sandbox secret list command', () => { listSecretsResponseMock ); const sandboxIdResolver: SandboxBackendIdResolver = { - resolve: (nameOverride?: string) => + resolve: (identifier?: string) => Promise.resolve({ namespace: testBackendId, - name: nameOverride || testSandboxName, + name: identifier || testSandboxName, type: 'sandbox', }), } as SandboxBackendIdResolver; @@ -70,7 +70,7 @@ void describe('sandbox secret list command', () => { }); void it('lists secrets for named sandbox', async () => { - await commandRunner.runCommand('list --name anotherName'); + await commandRunner.runCommand('list --identifier anotherName'); assert.equal(secretListMock.mock.callCount(), 1); assert.deepStrictEqual(secretListMock.mock.calls[0].arguments[0], { 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 index eaf6f3b7fc..1bde9b2fcb 100644 --- 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 @@ -40,7 +40,7 @@ export class SandboxSecretListCommand args: ArgumentsCamelCase ): Promise => { const sandboxBackendIdentifier = await this.sandboxIdResolver.resolve( - args.name + args.identifier ); const secrets = await this.secretClient.listSecrets( sandboxBackendIdentifier 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 index 09b1851bbf..273ce307e0 100644 --- 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 @@ -19,10 +19,10 @@ void describe('sandbox secret remove command', () => { ); const sandboxIdResolver: SandboxBackendIdResolver = { - resolve: (nameOverride?: string) => + resolve: (identifier?: string) => Promise.resolve({ namespace: testBackendId, - name: nameOverride || testSandboxName, + name: identifier || testSandboxName, type: 'sandbox', }), } as SandboxBackendIdResolver; @@ -57,7 +57,7 @@ void describe('sandbox secret remove command', () => { void it('removes secret from named sandbox', async () => { await commandRunner.runCommand( - `remove ${testSecretName} --name anotherName` + `remove ${testSecretName} --identifier anotherName` ); assert.equal(secretRemoveMock.mock.callCount(), 1); assert.deepStrictEqual(secretRemoveMock.mock.calls[0].arguments, [ 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 index 4001094899..4e12aa7c10 100644 --- 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 @@ -38,7 +38,7 @@ export class SandboxSecretRemoveCommand args: ArgumentsCamelCase ): Promise => { const sandboxBackendIdentifier = await this.sandboxIdResolver.resolve( - args.name + args.identifier ); await this.secretClient.removeSecret( sandboxBackendIdentifier, 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 index 2254e5f214..ddc482922e 100644 --- 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 @@ -26,10 +26,10 @@ void describe('sandbox secret set command', () => { ); const sandboxIdResolver: SandboxBackendIdResolver = { - resolve: (nameOverride?: string) => + resolve: (identifier?: string) => Promise.resolve({ namespace: testBackendId, - name: nameOverride || testSandboxName, + name: identifier || testSandboxName, type: 'sandbox', }), } as SandboxBackendIdResolver; @@ -78,7 +78,9 @@ void describe('sandbox secret set command', () => { () => Promise.resolve(testSecretValue) ); - await commandRunner.runCommand(`set ${testSecretName} --name anotherName`); + await commandRunner.runCommand( + `set ${testSecretName} --identifier anotherName` + ); assert.equal(mockSecretValue.mock.callCount(), 1); assert.equal(secretSetMock.mock.callCount(), 1); 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 index 81292528bf..8dfe58ef7b 100644 --- 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 @@ -41,7 +41,7 @@ export class SandboxSecretSetCommand ): Promise => { const secretVal = await AmplifyPrompter.secretValue(); await this.secretClient.setSecret( - await this.sandboxIdResolver.resolve(args.name), + await this.sandboxIdResolver.resolve(args.identifier), args.secretName, secretVal ); diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 2f9e8b5ceb..4d55c50242 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -37,7 +37,7 @@ void describe('sandbox command', () => { generateClientConfigToFile: clientConfigGenerationMock, } as unknown as ClientConfigGeneratorAdapter; - const commandMiddleware = new CommandMiddleware(); + const commandMiddleware = new CommandMiddleware(printer); const mockHandleProfile = mock.method( commandMiddleware, 'ensureAwsCredentialAndRegion', @@ -94,21 +94,21 @@ void describe('sandbox command', () => { void it('starts sandbox without any additional flags', async () => { await commandRunner.runCommand('sandbox'); assert.equal(sandboxStartMock.mock.callCount(), 1); - assert.ok(!sandboxStartMock.mock.calls[0].arguments[0].name); + assert.ok(!sandboxStartMock.mock.calls[0].arguments[0].identifier); }); - void it('starts sandbox with user provided app name', async () => { - await commandRunner.runCommand('sandbox --name user-app-name'); + void it('starts sandbox with user provided sandbox identifier', async () => { + await commandRunner.runCommand('sandbox --identifier user-app-name'); assert.equal(sandboxStartMock.mock.callCount(), 1); assert.deepStrictEqual( - sandboxStartMock.mock.calls[0].arguments[0].name, + sandboxStartMock.mock.calls[0].arguments[0].identifier, 'user-app-name' ); }); void it('shows available options in help output', async () => { const output = await commandRunner.runCommand('sandbox --help'); - assert.match(output, /--name/); + assert.match(output, /--identifier/); assert.match(output, /--dir-to-watch/); assert.match(output, /--exclude/); assert.match(output, /--config-format/); diff --git a/packages/cli/src/commands/sandbox/sandbox_command.ts b/packages/cli/src/commands/sandbox/sandbox_command.ts index ce87495202..92d04cf907 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.ts @@ -35,7 +35,7 @@ export type SandboxEventHandlers = { }; export type SandboxEventHandlerParams = { - sandboxName?: string; + sandboxIdentifier?: string; clientConfigLifecycleHandler: ClientConfigLifecycleHandler; }; @@ -59,7 +59,7 @@ export class SandboxCommand */ readonly describe: string; - private sandboxName?: string; + private sandboxIdentifier?: string; /** * Creates sandbox command. @@ -82,7 +82,7 @@ export class SandboxCommand args: ArgumentsCamelCase ): Promise => { const sandbox = await this.sandboxFactory.getInstance(); - this.sandboxName = args.name; + this.sandboxIdentifier = args.identifier; // attaching event handlers const clientConfigLifecycleHandler = new ClientConfigLifecycleHandler( @@ -92,7 +92,7 @@ export class SandboxCommand args.configFormat ); const eventHandlers = this.sandboxEventHandlerCreator?.({ - sandboxName: this.sandboxName, + sandboxIdentifier: this.sandboxIdentifier, clientConfigLifecycleHandler, }); if (eventHandlers) { @@ -113,7 +113,7 @@ export class SandboxCommand await sandbox.start({ dir: args.dirToWatch, exclude: watchExclusions, - name: args.name, + identifier: args.identifier, profile: args.profile, }); process.once('SIGINT', () => void this.sigIntHandler()); @@ -142,9 +142,9 @@ export class SandboxCommand array: true, global: false, }) - .option('name', { + .option('identifier', { describe: - 'An optional name to distinguish between different sandboxes. Default is the name of the system user executing the process', + 'An optional identifier to distinguish between different sandboxes. Default is the name of the system user executing the process', type: 'string', array: false, }) @@ -179,11 +179,11 @@ export class SandboxCommand if (argv['dir-to-watch']) { await this.validateDirectory('dir-to-watch', argv['dir-to-watch']); } - if (argv.name) { - const projectNameRegex = /^[a-zA-Z0-9-]{1,15}$/; - if (!argv.name.match(projectNameRegex)) { + if (argv.identifier) { + const identifierRegex = /^[a-zA-Z0-9-]{1,15}$/; + if (!argv.identifier.match(identifierRegex)) { throw new Error( - `--name should match [a-zA-Z0-9-] and be less than 15 characters.` + `--identifier should match [a-zA-Z0-9-] and be less than 15 characters.` ); } } @@ -202,7 +202,7 @@ export class SandboxCommand if (answer) await ( await this.sandboxFactory.getInstance() - ).delete({ name: this.sandboxName }); + ).delete({ identifier: this.sandboxIdentifier }); }; private validateDirectory = async (option: string, dir: string) => { diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index 194e2b53be..111ed4e891 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -49,7 +49,7 @@ export const createSandboxCommand = (): CommandModule< async () => await new UsageDataEmitterFactory().getInstance(libraryVersion) ); - const commandMiddleWare = new CommandMiddleware(); + const commandMiddleWare = new CommandMiddleware(printer); return new SandboxCommand( sandboxFactory, [new SandboxDeleteCommand(sandboxFactory), createSandboxSecretCommand()], diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts index 9d5d37df6f..dfe0775742 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts @@ -58,7 +58,7 @@ void describe('sandbox_event_handler_factory', () => { await Promise.all( eventFactory .getSandboxEventHandlers({ - sandboxName: 'my-app', + sandboxIdentifier: 'my-app', clientConfigLifecycleHandler, }) .successfulDeployment.map((e) => @@ -96,7 +96,7 @@ void describe('sandbox_event_handler_factory', () => { await Promise.all( eventFactory .getSandboxEventHandlers({ - sandboxName: 'my-app', + sandboxIdentifier: 'my-app', clientConfigLifecycleHandler, }) .failedDeployment.map((e) => e(testError)) @@ -119,7 +119,7 @@ void describe('sandbox_event_handler_factory', () => { await Promise.all( eventFactory .getSandboxEventHandlers({ - sandboxName: 'my-app', + sandboxIdentifier: 'my-app', clientConfigLifecycleHandler, }) .failedDeployment.map((e) => e(testError)) @@ -165,7 +165,7 @@ void describe('sandbox_event_handler_factory', () => { await Promise.all( eventFactory .getSandboxEventHandlers({ - sandboxName: 'my-app', + sandboxIdentifier: 'my-app', clientConfigLifecycleHandler, }) .successfulDeployment.map((e) => e()) @@ -207,7 +207,7 @@ void describe('sandbox_event_handler_factory', () => { await Promise.all( eventFactory .getSandboxEventHandlers({ - sandboxName: 'my-app', + sandboxIdentifier: 'my-app', clientConfigLifecycleHandler, }) .successfulDeletion.map((e) => e()) diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts index a2e8700e8a..33d76ca797 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts @@ -13,20 +13,20 @@ export class SandboxEventHandlerFactory { */ constructor( private readonly getBackendIdentifier: ( - sandboxName?: string + sandboxIdentifier?: string ) => Promise, private readonly getUsageDataEmitter: () => Promise ) {} getSandboxEventHandlers: SandboxEventHandlerCreator = ({ - sandboxName, + sandboxIdentifier: sandboxIdentifier, clientConfigLifecycleHandler, }) => { return { successfulDeployment: [ async (...args: unknown[]) => { const backendIdentifier = await this.getBackendIdentifier( - sandboxName + sandboxIdentifier ); const usageDataEmitter = await this.getUsageDataEmitter(); try { diff --git a/packages/cli/src/commands/sandbox/sandbox_id_resolver.ts b/packages/cli/src/commands/sandbox/sandbox_id_resolver.ts index aa63c3f064..79df7744e7 100644 --- a/packages/cli/src/commands/sandbox/sandbox_id_resolver.ts +++ b/packages/cli/src/commands/sandbox/sandbox_id_resolver.ts @@ -17,9 +17,9 @@ export class SandboxBackendIdResolver { /** * Returns a concatenation of the resolved appName and the current username */ - resolve = async (nameOverride?: string): Promise => { + resolve = async (identifier?: string): Promise => { const namespace = await this.namespaceResolver.resolve(); - const name = nameOverride || this.userInfo().username; + const name = identifier || this.userInfo().username; return { namespace, diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 6c14fe104d..708bb87b83 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -149,7 +149,7 @@ export type ClientConfig = clientConfigTypesV1.AWSAmplifyBackendOutputs; // @public (undocumented) export enum ClientConfigFileBaseName { // (undocumented) - DEFAULT = "amplify-outputs", + DEFAULT = "amplify_outputs", // (undocumented) LEGACY = "amplifyconfiguration" } diff --git a/packages/client-config/src/client-config-types/client_config.ts b/packages/client-config/src/client-config-types/client_config.ts index e621755ab9..c5410ed937 100644 --- a/packages/client-config/src/client-config-types/client_config.ts +++ b/packages/client-config/src/client-config-types/client_config.ts @@ -69,7 +69,7 @@ export enum ClientConfigFormat { export enum ClientConfigFileBaseName { LEGACY = 'amplifyconfiguration', - DEFAULT = 'amplify-outputs', + DEFAULT = 'amplify_outputs', } export type GenerateClientConfigToFileResult = { diff --git a/packages/create-amplify/src/gitignore_initializer.test.ts b/packages/create-amplify/src/gitignore_initializer.test.ts index 1f74332530..8792b0893b 100644 --- a/packages/create-amplify/src/gitignore_initializer.test.ts +++ b/packages/create-amplify/src/gitignore_initializer.test.ts @@ -26,6 +26,7 @@ void describe('GitIgnoreInitializer', () => { `# amplify${os.EOL}`, `node_modules${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -55,6 +56,7 @@ void describe('GitIgnoreInitializer', () => { os.EOL + os.EOL, `# amplify${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -80,6 +82,7 @@ void describe('GitIgnoreInitializer', () => { os.EOL, `# amplify${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -105,6 +108,7 @@ void describe('GitIgnoreInitializer', () => { `${os.EOL}${os.EOL}`, `# amplify${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -130,6 +134,7 @@ void describe('GitIgnoreInitializer', () => { `${os.EOL}${os.EOL}`, `# amplify${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -155,6 +160,7 @@ void describe('GitIgnoreInitializer', () => { `${os.EOL}${os.EOL}`, `# amplify${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); @@ -189,6 +195,7 @@ void describe('GitIgnoreInitializer', () => { `# amplify${os.EOL}`, `node_modules${os.EOL}`, `.amplify${os.EOL}`, + `amplify_outputs*${os.EOL}`, `amplifyconfiguration*${os.EOL}`, ]; await gitIgnoreInitializer.ensureInitialized(); diff --git a/packages/create-amplify/src/gitignore_initializer.ts b/packages/create-amplify/src/gitignore_initializer.ts index 1ada737b37..b53c92a5da 100644 --- a/packages/create-amplify/src/gitignore_initializer.ts +++ b/packages/create-amplify/src/gitignore_initializer.ts @@ -28,6 +28,7 @@ export class GitIgnoreInitializer { '# amplify', 'node_modules', '.amplify', + 'amplify_outputs*', 'amplifyconfiguration*', ]; const gitIgnoreContent = await this.getGitIgnoreContent(); diff --git a/packages/integration-tests/src/test-e2e/create_amplify.test.ts b/packages/integration-tests/src/test-e2e/create_amplify.test.ts index 7aae65774f..52ac026f79 100644 --- a/packages/integration-tests/src/test-e2e/create_amplify.test.ts +++ b/packages/integration-tests/src/test-e2e/create_amplify.test.ts @@ -169,6 +169,7 @@ void describe( assert.deepStrictEqual(gitIgnoreContent.sort(), [ '# amplify', '.amplify', + 'amplify_outputs*', 'amplifyconfiguration*', 'node_modules', ]); diff --git a/packages/sandbox/API.md b/packages/sandbox/API.md index f327beb929..abfe95586f 100644 --- a/packages/sandbox/API.md +++ b/packages/sandbox/API.md @@ -12,7 +12,7 @@ import EventEmitter from 'events'; import { Printer } from '@aws-amplify/cli-core'; // @public (undocumented) -export type BackendIdSandboxResolver = (sandboxName?: string) => Promise; +export type BackendIdSandboxResolver = (identifier?: string) => Promise; // @public export type Sandbox = { @@ -23,7 +23,7 @@ export type Sandbox = { // @public (undocumented) export type SandboxDeleteOptions = { - name?: string; + identifier?: string; }; // @public (undocumented) @@ -33,7 +33,7 @@ export type SandboxEvents = 'successfulDeployment' | 'failedDeployment' | 'succe export type SandboxOptions = { dir?: string; exclude?: string[]; - name?: string; + identifier?: string; format?: ClientConfigFormat; profile?: string; }; diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index 5c3bb2d12e..4a02a2c7dc 100644 --- a/packages/sandbox/src/file_watching_sandbox.test.ts +++ b/packages/sandbox/src/file_watching_sandbox.test.ts @@ -22,6 +22,7 @@ import { LogLevel, PackageManagerControllerFactory, Printer, + format, } from '@aws-amplify/cli-core'; import { fileURLToPath } from 'url'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; @@ -145,6 +146,10 @@ void describe('Sandbox to check if region is bootstrapped', () => { backendDeployerDestroyMock.mock.resetCalls(); backendDeployerDeployMock.mock.resetCalls(); await sandboxInstance.stop(); + + // Printer mocks are reset after the sandbox stop to reset the "Shutting down" call as well. + printer.log.mock.resetCalls(); + printer.print.mock.resetCalls(); }); void it('when region has not bootstrapped, then opens console to initiate bootstrap', async () => { @@ -226,6 +231,32 @@ void describe('Sandbox using local project name resolver', () => { subscribeMock.mock.resetCalls(); cfnClientSendMock.mock.resetCalls(); await sandboxInstance.stop(); + + // Printer mocks are reset after the sandbox stop to reset the "Shutting down" call as well. + printer.log.mock.resetCalls(); + printer.print.mock.resetCalls(); + }); + + void it('correctly displays the sandbox name at the startup and helper message when --identifier is not provided', async () => { + ({ sandboxInstance } = await setupAndStartSandbox( + { + executor: sandboxExecutor, + cfnClient: cfnClientMock, + }, + false + )); + assert.strictEqual(printer.log.mock.callCount(), 7); + + assert.strictEqual( + printer.log.mock.calls[1].arguments[0], + format.indent(`${format.bold('Identifier:')} \ttestSandboxName`) + ); + assert.strictEqual( + printer.log.mock.calls[3].arguments[0], + `${format.indent( + format.dim('\nTo specify a different sandbox identifier, use ') + )}${format.bold('--identifier')}` + ); }); void it('makes initial deployment without type checking at start if no typescript file is present', async () => { @@ -665,7 +696,7 @@ void describe('Sandbox using local project name resolver', () => { cfnClient: cfnClientMock, name: 'customSandboxName', })); - await sandboxInstance.delete({ name: 'customSandboxName' }); + await sandboxInstance.delete({ identifier: 'customSandboxName' }); // BackendDeployer should be called once assert.strictEqual(backendDeployerDestroyMock.mock.callCount(), 1); @@ -818,7 +849,7 @@ const setupAndStartSandbox = async ( await sandboxInstance.start({ dir: testData.dir, exclude: testData.exclude, - name: testData.name, + identifier: testData.name, format: testData.format, profile: testData.profile, }); diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts index 1b2e4c6cbe..8e1ffba48d 100644 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ b/packages/sandbox/src/file_watching_sandbox.ts @@ -29,7 +29,10 @@ import { FilesChangesTracker, createFilesChangesTracker, } from './files_changes_tracker.js'; -import { AmplifyError } from '@aws-amplify/platform-core'; +import { + AmplifyError, + BackendIdentifierConversions, +} from '@aws-amplify/platform-core'; export const CDK_BOOTSTRAP_STACK_NAME = 'CDKToolkit'; export const CDK_BOOTSTRAP_VERSION_KEY = 'BootstrapVersion'; @@ -105,7 +108,8 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { this.outputFilesExcludedFromWatch = this.outputFilesExcludedFromWatch.concat(...ignoredPaths); - this.printer.log(`[Sandbox] Initializing...`, LogLevel.DEBUG); + await this.printSandboxNameInfo(options.identifier); + // Since 'cdk deploy' is a relatively slow operation for a 'watch' process, // introduce a concurrency latch that tracks the state. // This way, if file change events arrive when a 'cdk deploy' is still executing, @@ -191,7 +195,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { '[Sandbox] Deleting all the resources in the sandbox environment...' ); await this.executor.destroy( - await this.backendIdSandboxResolver(options.name) + await this.backendIdSandboxResolver(options.identifier) ); this.emit('successfulDeletion'); this.printer.log('[Sandbox] Finished deleting.'); @@ -212,7 +216,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { private deploy = async (options: SandboxOptions) => { try { const deployResult = await this.executor.deploy( - await this.backendIdSandboxResolver(options.name), + await this.backendIdSandboxResolver(options.identifier), // It's important to pass this as callback so that debounce does // not reset tracker prematurely this.shouldValidateAppSources @@ -238,7 +242,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { }; private reset = async (options: SandboxOptions) => { - await this.delete({ name: options.name }); + await this.delete({ identifier: options.identifier }); await this.start(options); }; @@ -351,4 +355,26 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { } // else let the sandbox continue so customers can revert their changes }; + + private printSandboxNameInfo = async (sandboxIdentifier?: string) => { + const sandboxBackendId = await this.backendIdSandboxResolver( + sandboxIdentifier + ); + const stackName = + BackendIdentifierConversions.toStackName(sandboxBackendId); + this.printer.log( + format.indent(format.highlight(format.bold('\nAmplify Sandbox\n'))) + ); + this.printer.log( + format.indent(`${format.bold('Identifier:')} \t${sandboxBackendId.name}`) + ); + this.printer.log(format.indent(`${format.bold('Stack:')} \t${stackName}`)); + if (!sandboxIdentifier) { + this.printer.log( + `${format.indent( + format.dim('\nTo specify a different sandbox identifier, use ') + )}${format.bold('--identifier')}` + ); + } + }; } diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index e506338b60..5cce2eb489 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -33,14 +33,14 @@ export type SandboxEvents = export type SandboxOptions = { dir?: string; exclude?: string[]; - name?: string; + identifier?: string; format?: ClientConfigFormat; profile?: string; }; export type SandboxDeleteOptions = { - name?: string; + identifier?: string; }; export type BackendIdSandboxResolver = ( - sandboxName?: string + identifier?: string ) => Promise;