From ee3d55fead9bc06df0efc245085372b3165ad045 Mon Sep 17 00:00:00 2001 From: Spencer Stolworthy Date: Wed, 27 Sep 2023 10:28:18 -0700 Subject: [PATCH] feat: sandbox deployment events (#254) feat: add deployment events to sandbox --- .changeset/moody-rats-wonder.md | 6 + .../client_config_generator_adapter.ts | 0 .../config/generate_config_command.test.ts | 2 +- .../config/generate_config_command.ts | 2 +- .../generate/generate_command_factory.ts | 2 +- .../commands/sandbox/sandbox_command.test.ts | 41 +++-- .../src/commands/sandbox/sandbox_command.ts | 49 ++++- .../sandbox/sandbox_command_factory.ts | 45 ++++- packages/client-config/src/index.ts | 1 + .../process-controller/process_controller.ts | 2 + packages/sandbox/API.md | 9 +- .../config/client_config_generator_adapter.ts | 34 ---- .../sandbox/src/file_watching_sandbox.test.ts | 173 ++---------------- packages/sandbox/src/file_watching_sandbox.ts | 74 +++----- packages/sandbox/src/sandbox.ts | 11 +- .../sandbox/src/sandbox_singleton_factory.ts | 10 +- 16 files changed, 172 insertions(+), 289 deletions(-) create mode 100644 .changeset/moody-rats-wonder.md rename packages/cli/src/{commands/generate/config => client-config}/client_config_generator_adapter.ts (100%) delete mode 100644 packages/sandbox/src/config/client_config_generator_adapter.ts diff --git a/.changeset/moody-rats-wonder.md b/.changeset/moody-rats-wonder.md new file mode 100644 index 0000000000..dd57e88525 --- /dev/null +++ b/.changeset/moody-rats-wonder.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/sandbox': minor +'@aws-amplify/backend-cli': minor +--- + +Add event handlers for Sandbox diff --git a/packages/cli/src/commands/generate/config/client_config_generator_adapter.ts b/packages/cli/src/client-config/client_config_generator_adapter.ts similarity index 100% rename from packages/cli/src/commands/generate/config/client_config_generator_adapter.ts rename to packages/cli/src/client-config/client_config_generator_adapter.ts diff --git a/packages/cli/src/commands/generate/config/generate_config_command.test.ts b/packages/cli/src/commands/generate/config/generate_config_command.test.ts index 9d0b21db0b..aa1a7fd139 100644 --- a/packages/cli/src/commands/generate/config/generate_config_command.test.ts +++ b/packages/cli/src/commands/generate/config/generate_config_command.test.ts @@ -8,8 +8,8 @@ import { TestCommandRunner, } from '../../../test-utils/command_runner.js'; import assert from 'node:assert'; -import { ClientConfigGeneratorAdapter } from './client_config_generator_adapter.js'; import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; +import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js'; void describe('generate config command', () => { const clientConfigGeneratorAdapter = new ClientConfigGeneratorAdapter( 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 ddc0f03d67..3d2b2086e1 100644 --- a/packages/cli/src/commands/generate/config/generate_config_command.ts +++ b/packages/cli/src/commands/generate/config/generate_config_command.ts @@ -1,7 +1,7 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { ClientConfigFormat } from '@aws-amplify/client-config'; -import { ClientConfigGeneratorAdapter } from './client_config_generator_adapter.js'; import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; +import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js'; export type GenerateConfigCommandOptions = { stack: string | undefined; diff --git a/packages/cli/src/commands/generate/generate_command_factory.ts b/packages/cli/src/commands/generate/generate_command_factory.ts index 38523a768b..19f602440e 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.ts @@ -2,12 +2,12 @@ import { CommandModule } from 'yargs'; import { GenerateCommand } from './generate_command.js'; import { GenerateConfigCommand } from './config/generate_config_command.js'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; -import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js'; import { GenerateFormsCommand } from './forms/generate_forms_command.js'; import { CwdPackageJsonLoader } from '../../cwd_package_json_loader.js'; import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js'; import { LocalAppNameResolver } from '../../backend-identifier/local_app_name_resolver.js'; import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js'; +import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; import { FormGenerationHandler } from './forms/form_generation_handler.js'; import { BackendOutputClient } from '@aws-amplify/deployed-backend-client'; import { GenerateApiCodeAdapter } from './graphql-client-code/generate_api_code_adapter.js'; diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 62d5c93da7..c78266d4e0 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -7,10 +7,11 @@ import { } from '../../test-utils/command_runner.js'; import assert from 'node:assert'; import fs from 'fs'; -import { SandboxCommand } from './sandbox_command.js'; +import { EventHandler, 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 { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js'; void describe('sandbox command factory', () => { @@ -23,6 +24,9 @@ void describe('sandbox command', () => { let commandRunner: TestCommandRunner; let sandbox: Sandbox; let sandboxStartMock = mock.fn(); + const mockGenerate = + mock.fn(); + const generationMock = mock.fn(); beforeEach(async () => { const sandboxFactory = new SandboxSingletonFactory(() => @@ -32,13 +36,25 @@ void describe('sandbox command', () => { sandboxStartMock = mock.method(sandbox, 'start', () => Promise.resolve()); const sandboxDeleteCommand = new SandboxDeleteCommand(sandboxFactory); - const sandboxCommand = new SandboxCommand(sandboxFactory, [ - sandboxDeleteCommand, - createSandboxSecretCommand(), - ]); + + const sandboxCommand = new SandboxCommand( + sandboxFactory, + [sandboxDeleteCommand, createSandboxSecretCommand()], + () => ({ + successfulDeployment: [generationMock], + }) + ); const parser = yargs().command(sandboxCommand as unknown as CommandModule); commandRunner = new TestCommandRunner(parser); sandboxStartMock.mock.resetCalls(); + mockGenerate.mock.resetCalls(); + }); + + void it('registers a callback on the "successfulDeployment" event', async () => { + const mockOn = mock.method(sandbox, 'on'); + await commandRunner.runCommand('sandbox'); + assert.equal(mockOn.mock.calls[0].arguments[0], 'successfulDeployment'); + assert.equal(mockOn.mock.calls[0].arguments[1], generationMock); }); void it('starts sandbox without any additional flags', async () => { @@ -56,21 +72,6 @@ void describe('sandbox command', () => { ); }); - void it('starts sandbox with user provided output directory for client config', async () => { - await commandRunner.runCommand( - 'sandbox --outDir test/location --format js' - ); - assert.equal(sandboxStartMock.mock.callCount(), 1); - assert.deepStrictEqual( - sandboxStartMock.mock.calls[0].arguments[0].clientConfigFilePath, - 'test/location' - ); - assert.deepStrictEqual( - sandboxStartMock.mock.calls[0].arguments[0].format, - 'js' - ); - }); - void it('shows available options in help output', async () => { const output = await commandRunner.runCommand('sandbox --help'); assert.match(output, /--name/); diff --git a/packages/cli/src/commands/sandbox/sandbox_command.ts b/packages/cli/src/commands/sandbox/sandbox_command.ts index 64a2e2cba8..22b28f24e5 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.ts @@ -1,8 +1,11 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; -import { ClientConfigFormat } from '@aws-amplify/client-config'; import fs from 'fs'; import { AmplifyPrompter } from '../prompter/amplify_prompts.js'; import { SandboxSingletonFactory } from '@aws-amplify/sandbox'; +import { + ClientConfigFormat, + getClientConfigPath, +} from '@aws-amplify/client-config'; export type SandboxCommandOptions = { dirToWatch: string | undefined; @@ -13,6 +16,22 @@ export type SandboxCommandOptions = { profile: string | undefined; }; +export type EventHandler = () => void; + +export type SandboxEventHandlers = { + successfulDeployment: EventHandler[]; +}; + +export type SandboxEventHandlerParams = { + appName?: string; + outDir?: string; + format?: ClientConfigFormat; +}; + +export type SandboxEventHandlerCreator = ( + params: SandboxEventHandlerParams +) => SandboxEventHandlers; + /** * Command that starts sandbox. */ @@ -36,7 +55,8 @@ export class SandboxCommand */ constructor( private readonly sandboxFactory: SandboxSingletonFactory, - private readonly sandboxSubCommands: CommandModule[] + private readonly sandboxSubCommands: CommandModule[], + private readonly sandboxEventHandlerCreator?: SandboxEventHandlerCreator ) { this.command = 'sandbox'; this.describe = 'Starts sandbox, watch mode for amplify deployments'; @@ -48,15 +68,28 @@ export class SandboxCommand handler = async ( args: ArgumentsCamelCase ): Promise => { + const sandbox = await this.sandboxFactory.getInstance(); this.appName = args.name; - await ( - await this.sandboxFactory.getInstance() - ).start({ + const eventHandlers = this.sandboxEventHandlerCreator?.({ + appName: args.name, + format: args.format, + outDir: args.outDir, + }); + if (eventHandlers) { + Object.entries(eventHandlers).forEach(([event, handlers]) => { + handlers.forEach((handler) => sandbox.on(event, handler)); + }); + } + const watchExclusions = args.exclude ?? []; + const clientConfigWritePath = await getClientConfigPath( + args.outDir, + args.format + ); + watchExclusions.push(clientConfigWritePath); + await sandbox.start({ dir: args.dirToWatch, - exclude: args.exclude, + exclude: watchExclusions, name: args.name, - format: args.format, - clientConfigFilePath: args.outDir, profile: args.profile, }); process.once('SIGINT', () => void this.sigIntHandler()); diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index 56b0681c00..40363c9d9c 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -1,10 +1,15 @@ import { CommandModule } from 'yargs'; - -import { SandboxCommand, SandboxCommandOptions } from './sandbox_command.js'; +import { + SandboxCommand, + SandboxCommandOptions, + SandboxEventHandlerCreator, +} from './sandbox_command.js'; import { SandboxSingletonFactory } from '@aws-amplify/sandbox'; 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 { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { LocalAppNameResolver } from '../../backend-identifier/local_app_name_resolver.js'; import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js'; @@ -15,13 +20,39 @@ export const createSandboxCommand = (): CommandModule< object, SandboxCommandOptions > => { + const credentialProvider = fromNodeProviderChain(); const sandboxIdResolver = new SandboxIdResolver( new LocalAppNameResolver(new CwdPackageJsonLoader()) ); const sandboxFactory = new SandboxSingletonFactory(sandboxIdResolver.resolve); - - return new SandboxCommand(sandboxFactory, [ - new SandboxDeleteCommand(sandboxFactory), - createSandboxSecretCommand(), - ]); + const clientConfigGeneratorAdapter = new ClientConfigGeneratorAdapter( + credentialProvider + ); + const getBackendIdentifier = async (appName?: string) => { + const sandboxId = appName ?? (await sandboxIdResolver.resolve()); + return { backendId: sandboxId, branchName: 'sandbox' }; + }; + const sandboxEventHandlerCreator: SandboxEventHandlerCreator = ({ + appName, + outDir, + format, + }) => { + return { + successfulDeployment: [ + async () => { + const id = await getBackendIdentifier(appName); + await clientConfigGeneratorAdapter.generateClientConfigToFile( + id, + outDir, + format + ); + }, + ], + }; + }; + return new SandboxCommand( + sandboxFactory, + [new SandboxDeleteCommand(sandboxFactory), createSandboxSecretCommand()], + sandboxEventHandlerCreator + ); }; diff --git a/packages/client-config/src/index.ts b/packages/client-config/src/index.ts index 17a9d8cfe9..69d9817ab8 100644 --- a/packages/client-config/src/index.ts +++ b/packages/client-config/src/index.ts @@ -4,3 +4,4 @@ export * from './client-config-types/client_config.js'; export * from './client-config-types/auth_client_config.js'; export * from './client-config-types/graphql_client_config.js'; export * from './client-config-types/storage_client_config.js'; +export * from './paths/get_client_config_path.js'; diff --git a/packages/integration-tests/src/process-controller/process_controller.ts b/packages/integration-tests/src/process-controller/process_controller.ts index 966938367c..fe46a2fae2 100644 --- a/packages/integration-tests/src/process-controller/process_controller.ts +++ b/packages/integration-tests/src/process-controller/process_controller.ts @@ -72,6 +72,8 @@ export class ProcessController { if (typeof currentInteraction.payload === 'string') { if (currentInteraction.payload === CONTROL_C) { if (process.platform.startsWith('win')) { + // Wait X milliseconds before sending kill in hopes of draining the node event queue + await new Promise((resolve) => setTimeout(resolve, 5000)); // turns out killing child process on Windows is a huge PITA // https://stackoverflow.com/questions/23706055/why-can-i-not-kill-my-child-process-in-nodejs-on-windows // https://github.com/sindresorhus/execa#killsignal-options diff --git a/packages/sandbox/API.md b/packages/sandbox/API.md index 8275c80fa1..e29520c7e4 100644 --- a/packages/sandbox/API.md +++ b/packages/sandbox/API.md @@ -4,20 +4,26 @@ ```ts +/// + import { ClientConfigFormat } from '@aws-amplify/client-config'; +import EventEmitter from 'events'; // @public export type Sandbox = { start: (options: SandboxOptions) => Promise; stop: () => Promise; delete: (options: SandboxDeleteOptions) => Promise; -}; +} & EventEmitter; // @public (undocumented) export type SandboxDeleteOptions = { name?: string; }; +// @public (undocumented) +export type SandboxEvents = 'successfulDeployment'; + // @public (undocumented) export type SandboxOptions = { dir?: string; @@ -25,7 +31,6 @@ export type SandboxOptions = { name?: string; format?: ClientConfigFormat; profile?: string; - clientConfigFilePath?: string; }; // @public diff --git a/packages/sandbox/src/config/client_config_generator_adapter.ts b/packages/sandbox/src/config/client_config_generator_adapter.ts deleted file mode 100644 index 072479c38b..0000000000 --- a/packages/sandbox/src/config/client_config_generator_adapter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - ClientConfigFormat, - generateClientConfigToFile, -} from '@aws-amplify/client-config'; -import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; -import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; - -/** - * Adapts static generateClientConfigToFile from @aws-amplify/client-config call to make it injectable and testable. - */ -export class ClientConfigGeneratorAdapter { - /** - * Creates new adapter for generateClientConfigToFile from @aws-amplify/client-config. - */ - constructor( - private readonly awsCredentialProvider: AwsCredentialIdentityProvider - ) {} - /** - * Calls generateClientConfigToFile from @aws-amplify/client-config. - * @see generateClientConfigToFile for more information. - */ - generateClientConfigToFile = async ( - backendIdentifier: BackendIdentifier, - outDir?: string, - format?: ClientConfigFormat - ): Promise => { - await generateClientConfigToFile( - this.awsCredentialProvider, - backendIdentifier, - outDir, - format - ); - }; -} diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index 10fc1b67a4..8f257323b4 100644 --- a/packages/sandbox/src/file_watching_sandbox.test.ts +++ b/packages/sandbox/src/file_watching_sandbox.test.ts @@ -1,16 +1,13 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test'; +import path from 'path'; import watcher from '@parcel/watcher'; -import { ClientConfigFormat } from '@aws-amplify/client-config'; import { FileWatchingSandbox } from './file_watching_sandbox.js'; import assert from 'node:assert'; import { AmplifySandboxExecutor } from './sandbox_executor.js'; -import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js'; -import * as path from 'path'; import { BackendDeployerFactory } from '@aws-amplify/backend-deployer'; import fs from 'fs'; import parseGitIgnore from 'parse-gitignore'; -const configFileName = 'amplifyconfiguration'; // Watcher mocks const unsubscribeMockFn = mock.fn(); const subscribeMock = mock.method(watcher, 'subscribe', async () => { @@ -18,16 +15,6 @@ const subscribeMock = mock.method(watcher, 'subscribe', async () => { }); let fileChangeEventActualFn: watcher.SubscribeCallback; -// Client config mocks -const clientConfigGeneratorAdapter = new ClientConfigGeneratorAdapter( - mock.fn() -); -const generateClientConfigMock = mock.method( - clientConfigGeneratorAdapter, - 'generateClientConfigToFile', - () => Promise.resolve('testClientConfig') -); - const backendDeployer = BackendDeployerFactory.getInstance(); const execaDeployMock = mock.method(backendDeployer, 'deploy', () => Promise.resolve() @@ -62,22 +49,15 @@ void describe('Sandbox using local project name resolver', () => { beforeEach(async () => { // ensures that .gitignore is set as absent mock.method(fs, 'existsSync', () => false); - sandboxInstance = new FileWatchingSandbox( - 'testSandboxId', - clientConfigGeneratorAdapter, - cdkExecutor - ); + sandboxInstance = new FileWatchingSandbox('testSandboxId', cdkExecutor); await sandboxInstance.start({ dir: 'testDir', exclude: ['exclude1', 'exclude2'], - clientConfigFilePath: path.join('test', 'location'), - format: ClientConfigFormat.JS, }); // At this point one deployment should already have been done on sandbox startup assert.strictEqual(execaDeployMock.mock.callCount(), 1); // and client config generated only once - assert.equal(generateClientConfigMock.mock.callCount(), 1); if ( subscribeMock.mock.calls[0].arguments[1] && @@ -89,14 +69,12 @@ void describe('Sandbox using local project name resolver', () => { // Reset all the calls to avoid extra startup call execaDestroyMock.mock.resetCalls(); execaDeployMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); }); afterEach(async () => { execaDestroyMock.mock.resetCalls(); execaDeployMock.mock.resetCalls(); subscribeMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); await sandboxInstance.stop(); }); @@ -108,17 +86,7 @@ void describe('Sandbox using local project name resolver', () => { // File watcher should be called with right arguments such as dir and excludes assert.strictEqual(subscribeMock.mock.calls[0].arguments[0], 'testDir'); assert.deepStrictEqual(subscribeMock.mock.calls[0].arguments[2], { - ignore: [ - 'cdk.out', - path.join( - process.cwd(), - 'test', - 'location', - `${configFileName}.${ClientConfigFormat.JS as string}` - ), - 'exclude1', - 'exclude2', - ], + ignore: ['cdk.out', 'exclude1', 'exclude2'], }); // CDK should be called once @@ -143,9 +111,6 @@ void describe('Sandbox using local project name resolver', () => { { type: 'create', path: 'foo/test3.ts' }, ]); assert.strictEqual(execaDeployMock.mock.callCount(), 1); - - // and client config generated only once - assert.equal(generateClientConfigMock.mock.callCount(), 1); }); void it('calls CDK once when multiple file changes are within few milliseconds (debounce)', async () => { @@ -155,9 +120,6 @@ void describe('Sandbox using local project name resolver', () => { { type: 'update', path: 'foo/test4.ts' }, ]); assert.strictEqual(execaDeployMock.mock.callCount(), 1); - - // and client config written only once - assert.equal(generateClientConfigMock.mock.callCount(), 1); }); void it('waits for file changes after completing a deployment and deploys again', async () => { @@ -168,9 +130,6 @@ void describe('Sandbox using local project name resolver', () => { { type: 'update', path: 'foo/test6.ts' }, ]); assert.strictEqual(execaDeployMock.mock.callCount(), 2); - - // and client config written twice as well - assert.equal(generateClientConfigMock.mock.callCount(), 2); }); void it('queues deployment if a file change is detected during an ongoing', async () => { @@ -193,27 +152,6 @@ void describe('Sandbox using local project name resolver', () => { await new Promise((resolve) => setTimeout(resolve, 500)); assert.strictEqual(execaDeployMock.mock.callCount(), 2); - assert.equal(generateClientConfigMock.mock.callCount(), 2); - }); - - void it('writes the correct client-config to default cwd path', async () => { - await fileChangeEventActualFn(null, [ - { type: 'update', path: 'foo/test1.ts' }, - ]); - - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - - // generate was called with right arguments - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[0], - { backendId: 'testSandboxId', branchName: 'sandbox' } - ); - - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join('test', 'location') - ); }); void it('calls CDK destroy when delete is called', async () => { @@ -266,17 +204,11 @@ void describe('Sandbox with user provided app name', () => { beforeEach(async () => { // ensures that .gitignore is set as absent mock.method(fs, 'existsSync', () => false); - sandboxInstance = new FileWatchingSandbox( - 'testSandboxId', - clientConfigGeneratorAdapter, - cdkExecutor - ); + sandboxInstance = new FileWatchingSandbox('testSandboxId', cdkExecutor); await sandboxInstance.start({ dir: 'testDir', exclude: ['exclude1', 'exclude2'], name: 'customSandboxName', - format: ClientConfigFormat.TS, - clientConfigFilePath: path.join('test', 'location'), }); if ( subscribeMock.mock.calls[0].arguments[1] && @@ -288,14 +220,12 @@ void describe('Sandbox with user provided app name', () => { // Reset all the calls to avoid extra startup call execaDestroyMock.mock.resetCalls(); execaDeployMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); }); afterEach(async () => { execaDestroyMock.mock.resetCalls(); execaDeployMock.mock.resetCalls(); subscribeMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); await sandboxInstance.stop(); }); @@ -307,17 +237,7 @@ void describe('Sandbox with user provided app name', () => { // File watcher should be called with right arguments such as dir and excludes assert.strictEqual(subscribeMock.mock.calls[0].arguments[0], 'testDir'); assert.deepStrictEqual(subscribeMock.mock.calls[0].arguments[2], { - ignore: [ - 'cdk.out', - path.join( - process.cwd(), - 'test', - 'location', - `${configFileName}.${ClientConfigFormat.TS as string}` - ), - 'exclude1', - 'exclude2', - ], + ignore: ['cdk.out', 'exclude1', 'exclude2'], }); // CDK should be called once @@ -334,9 +254,6 @@ void describe('Sandbox with user provided app name', () => { method: 'direct', }, ]); - - // and client config written only once - assert.equal(generateClientConfigMock.mock.callCount(), 1); }); void it('calls CDK destroy when delete is called with a user provided sandbox name', async () => { @@ -353,28 +270,6 @@ void describe('Sandbox with user provided app name', () => { }, ]); }); - - void it('writes the correct client-config to user provided path', async () => { - await fileChangeEventActualFn(null, [ - { type: 'update', path: 'foo/test1.ts' }, - ]); - - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - - // generate was called with right arguments - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[0], - { - backendId: 'customSandboxName', - branchName: 'sandbox', - } - ); - assert.equal( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join('test', 'location') - ); - }); }); void describe('Sandbox with absolute output path', () => { @@ -390,17 +285,11 @@ void describe('Sandbox with absolute output path', () => { beforeEach(async () => { // ensures that .gitignore is set as absent mock.method(fs, 'existsSync', () => false); - sandboxInstance = new FileWatchingSandbox( - 'testSandboxId', - clientConfigGeneratorAdapter, - cdkExecutor - ); + sandboxInstance = new FileWatchingSandbox('testSandboxId', cdkExecutor); await sandboxInstance.start({ dir: 'testDir', exclude: ['exclude1', 'exclude2'], name: 'customSandboxName', - format: ClientConfigFormat.JSON, - clientConfigFilePath: path.join('test', 'location'), profile: 'amplify-sandbox', }); if ( @@ -413,39 +302,15 @@ void describe('Sandbox with absolute output path', () => { // Reset all the calls to avoid extra startup call execaDeployMock.mock.resetCalls(); execaDestroyMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); }); afterEach(async () => { execaDeployMock.mock.resetCalls(); execaDestroyMock.mock.resetCalls(); subscribeMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); await sandboxInstance.stop(); }); - void it('generates client config at absolute location', async () => { - await fileChangeEventActualFn(null, [ - { type: 'update', path: 'foo/test1.ts' }, - ]); - - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - - // generate was called with right arguments - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[0], - { - backendId: 'customSandboxName', - branchName: 'sandbox', - } - ); - assert.equal( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join('test', 'location') - ); - }); - void it('sets AWS profile when starting sandbox', async () => { assert.strictEqual(process.env.AWS_PROFILE, 'amplify-sandbox'); }); @@ -475,17 +340,11 @@ void describe('Sandbox ignoring paths in .gitignore', () => { ], }; }); - sandboxInstance = new FileWatchingSandbox( - 'testSandboxId', - clientConfigGeneratorAdapter, - cdkExecutor - ); + sandboxInstance = new FileWatchingSandbox('testSandboxId', cdkExecutor); await sandboxInstance.start({ dir: 'testDir', exclude: ['customer_exclude1', 'customer_exclude2'], name: 'customSandboxName', - format: ClientConfigFormat.TS, - clientConfigFilePath: '', }); if ( subscribeMock.mock.calls[0].arguments[1] && @@ -497,14 +356,12 @@ void describe('Sandbox ignoring paths in .gitignore', () => { // Reset all the calls to avoid extra startup call execaDeployMock.mock.resetCalls(); execaDestroyMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); }); afterEach(async () => { execaDeployMock.mock.resetCalls(); execaDestroyMock.mock.resetCalls(); subscribeMock.mock.resetCalls(); - generateClientConfigMock.mock.resetCalls(); await sandboxInstance.stop(); }); @@ -518,10 +375,6 @@ void describe('Sandbox ignoring paths in .gitignore', () => { assert.deepStrictEqual(subscribeMock.mock.calls[0].arguments[2], { ignore: [ 'cdk.out', - path.join( - process.cwd(), - `${configFileName}.${ClientConfigFormat.TS as string}` - ), 'patternWithLeadingSlash', 'patternWithoutLeadingSlash', 'someFile.js', @@ -535,4 +388,16 @@ void describe('Sandbox ignoring paths in .gitignore', () => { // CDK should also be called once assert.strictEqual(execaDeployMock.mock.callCount(), 1); }); + void it('emits the successfulDeployment event after deployment', async () => { + const mockListener = mock.fn(); + const mockDeploy = mock.fn(); + mockDeploy.mock.mockImplementation(async () => null); + const executor: AmplifySandboxExecutor = { + deploy: mockDeploy, + } as unknown as AmplifySandboxExecutor; + const sandbox = new FileWatchingSandbox('my-sandbox', executor); + sandbox.on('successfulDeployment', mockListener); + await sandbox.start({}); + assert.equal(mockListener.mock.callCount(), 1); + }); }); diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts index 6207dac489..9d2648e630 100644 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ b/packages/sandbox/src/file_watching_sandbox.ts @@ -1,18 +1,21 @@ import debounce from 'debounce-promise'; import parcelWatcher, { subscribe } from '@parcel/watcher'; -import { ClientConfigFormat } from '@aws-amplify/client-config'; -import { getClientConfigPath } from '@aws-amplify/client-config/paths'; import { AmplifySandboxExecutor } from './sandbox_executor.js'; -import { Sandbox, SandboxDeleteOptions, SandboxOptions } from './sandbox.js'; -import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js'; +import { + Sandbox, + SandboxDeleteOptions, + SandboxEvents, + SandboxOptions, +} from './sandbox.js'; import parseGitIgnore from 'parse-gitignore'; import path from 'path'; import fs from 'fs'; +import EventEmitter from 'events'; /** * Runs a file watcher and deploys */ -export class FileWatchingSandbox implements Sandbox { +export class FileWatchingSandbox extends EventEmitter implements Sandbox { private watcherSubscription: Awaited>; private outputFilesExcludedFromWatch = ['cdk.out']; /** @@ -20,11 +23,28 @@ export class FileWatchingSandbox implements Sandbox { */ constructor( private readonly sandboxId: string, - private readonly clientConfigGenerator: ClientConfigGeneratorAdapter, private readonly executor: AmplifySandboxExecutor ) { process.once('SIGINT', () => void this.stop()); process.once('SIGTERM', () => void this.stop()); + super(); + } + + /** + * @inheritdoc + */ + override emit(eventName: SandboxEvents, ...args: unknown[]): boolean { + return super.emit(eventName, args); + } + + /** + * @inheritdoc + */ + override on( + eventName: SandboxEvents, + listener: (...args: unknown[]) => void + ): this { + return super.on(eventName, listener); } /** @@ -37,16 +57,9 @@ export class FileWatchingSandbox implements Sandbox { } const sandboxId = options.name ?? this.sandboxId; - const clientConfigWritePath = await getClientConfigPath( - options.clientConfigFilePath, - options.format - ); const ignoredPaths = this.getGitIgnoredPaths(); this.outputFilesExcludedFromWatch = - this.outputFilesExcludedFromWatch.concat( - clientConfigWritePath, - ...ignoredPaths - ); + this.outputFilesExcludedFromWatch.concat(...ignoredPaths); console.debug(`[Sandbox] Initializing...`); // Since 'cdk deploy' is a relatively slow operation for a 'watch' process, @@ -70,11 +83,6 @@ export class FileWatchingSandbox implements Sandbox { backendId: sandboxId, branchName: 'sandbox', }); - await this.writeUpdatedClientConfig( - sandboxId, - options.clientConfigFilePath, - options.format - ); // If latch is still 'deploying' after the 'await', that's fine, // but if it's 'queued', that means we need to deploy again @@ -89,14 +97,11 @@ export class FileWatchingSandbox implements Sandbox { backendId: sandboxId, branchName: 'sandbox', }); - await this.writeUpdatedClientConfig( - sandboxId, - options.clientConfigFilePath, - options.format - ); } latch = 'open'; this.emitWatching(); + console.debug('[Sandbox] Running successfulDeployment event handlers'); + this.emit('successfulDeployment'); }); this.watcherSubscription = await parcelWatcher.subscribe( @@ -155,27 +160,6 @@ export class FileWatchingSandbox implements Sandbox { console.log('[Sandbox] Finished deleting.'); }; - /** - * Runs post every deployment. Generates the client config and writes to a local file - * @param sandboxId for this sandbox execution. Either package.json#name + whoami or provided by user during `amplify sandbox` - * @param outDir optional location provided by customer to write client config to - * @param format optional format provided by customer to write client config in - */ - private writeUpdatedClientConfig = async ( - sandboxId: string, - outDir?: string, - format?: ClientConfigFormat - ) => { - await this.clientConfigGenerator.generateClientConfigToFile( - { - backendId: sandboxId, - branchName: 'sandbox', - }, - outDir, - format - ); - }; - /** * Just a shorthand console log to indicate whenever watcher is going idle */ diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 7ddeac24d3..3730792440 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -1,3 +1,4 @@ +import EventEmitter from 'events'; import { ClientConfigFormat } from '@aws-amplify/client-config'; /** @@ -19,7 +20,9 @@ export type Sandbox = { * Deletes this environment */ delete: (options: SandboxDeleteOptions) => Promise; -}; +} & EventEmitter; + +export type SandboxEvents = 'successfulDeployment'; export type SandboxOptions = { dir?: string; @@ -27,12 +30,6 @@ export type SandboxOptions = { name?: string; format?: ClientConfigFormat; profile?: string; - /** - * Optional path where client config should be generated for sandbox deployments - * If the path is relative, it is computed based on process.cwd() - * If the path is absolute, it is used as-is - */ - clientConfigFilePath?: string; }; export type SandboxDeleteOptions = { diff --git a/packages/sandbox/src/sandbox_singleton_factory.ts b/packages/sandbox/src/sandbox_singleton_factory.ts index 470dac1835..ba7984a6dd 100644 --- a/packages/sandbox/src/sandbox_singleton_factory.ts +++ b/packages/sandbox/src/sandbox_singleton_factory.ts @@ -1,7 +1,5 @@ import { FileWatchingSandbox } from './file_watching_sandbox.js'; import { Sandbox } from './sandbox.js'; -import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; -import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js'; import { BackendDeployerFactory } from '@aws-amplify/backend-deployer'; import { AmplifySandboxExecutor } from './sandbox_executor.js'; @@ -10,17 +8,12 @@ import { AmplifySandboxExecutor } from './sandbox_executor.js'; */ export class SandboxSingletonFactory { private instance: Sandbox | undefined; - private readonly clientConfigGenerator: ClientConfigGeneratorAdapter; /** * Initialize with an sandboxIdResolver. * This resolver will be called once and only once the first time getInstance() is called. * After that, the cached Sandbox instance is returned. */ - constructor(private readonly sandboxIdResolver: () => Promise) { - this.clientConfigGenerator = new ClientConfigGeneratorAdapter( - fromNodeProviderChain() - ); - } + constructor(private readonly sandboxIdResolver: () => Promise) {} /** * Returns a singleton instance of a Sandbox @@ -29,7 +22,6 @@ export class SandboxSingletonFactory { if (!this.instance) { this.instance = new FileWatchingSandbox( await this.sandboxIdResolver(), - this.clientConfigGenerator, new AmplifySandboxExecutor(BackendDeployerFactory.getInstance()) ); }