Skip to content

Commit

Permalink
feat(sandbox): check if region is bootstrapped
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhishek Raj committed Sep 27, 2023
1 parent e0e1488 commit 59a2a6e
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 22 deletions.
5 changes: 1 addition & 4 deletions packages/cli/src/commands/sandbox/sandbox_command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,6 @@ describe('sandbox command', () => {
it('starts sandbox with user provided AWS profile', async () => {
await commandRunner.runCommand('sandbox --profile amplify-sandbox');
assert.equal(sandboxStartMock.mock.callCount(), 1);
assert.deepStrictEqual(
sandboxStartMock.mock.calls[0].arguments[0].profile,
'amplify-sandbox'
);
assert.strictEqual(process.env.AWS_PROFILE, 'amplify-sandbox');
});
});
5 changes: 4 additions & 1 deletion packages/cli/src/commands/sandbox/sandbox_command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class SandboxCommand
args: ArgumentsCamelCase<SandboxCommandOptions>
): Promise<void> => {
this.appName = args.name;
const { profile } = args;
if (profile) {
process.env.AWS_PROFILE = profile;
}
await (
await this.sandboxFactory.getInstance()
).start({
Expand All @@ -58,7 +62,6 @@ export class SandboxCommand
name: args.name,
format: args.format,
clientConfigFilePath: args.outDir,
profile: args.profile,
});
process.once('SIGINT', () => void this.sigIntHandler());
};
Expand Down
4 changes: 3 additions & 1 deletion packages/sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"@aws-amplify/client-config": "0.2.0-alpha.5",
"@aws-amplify/deployed-backend-client": "^0.1.0",
"@aws-sdk/credential-providers": "^3.382.0",
"@aws-sdk/client-ssm": "^3.419.0",
"@aws-sdk/types": "^3.378.0",
"@parcel/watcher": "^2.3.0",
"debounce-promise": "^3.1.2",
"parse-gitignore": "^2.0.0"
"parse-gitignore": "^2.0.0",
"open": "^9.1.0"
},
"devDependencies": {
"@types/debounce-promise": "^3.1.6",
Expand Down
81 changes: 72 additions & 9 deletions packages/sandbox/src/file_watching_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as path from 'path';
import { BackendDeployerFactory } from '@aws-amplify/backend-deployer';
import fs from 'fs';
import parseGitIgnore from 'parse-gitignore';
import { SSMClient } from '@aws-sdk/client-ssm';
import open from 'open';

const configFileName = 'amplifyconfiguration';
// Watcher mocks
Expand All @@ -35,6 +37,15 @@ const execaDeployMock = mock.method(backendDeployer, 'deploy', () =>
const execaDestroyMock = mock.method(backendDeployer, 'destroy', () =>
Promise.resolve()
);
const ssmClientMock = new SSMClient({ region: 'test-region' });
const ssmClientSendMock = mock.fn();
mock.method(ssmClientMock, 'send', ssmClientSendMock);
ssmClientSendMock.mock.mockImplementation(() =>
Promise.resolve({
Parameters: [{ Name: '/cdk-bootstrap/foo/version' }, { name: 'testParam' }],
})
);
const openMock = mock.fn(open, (url: string) => Promise.resolve(url));

const testPath = path.join('test', 'location');
mock.method(fs, 'lstatSync', (path: string) => {
Expand All @@ -49,6 +60,51 @@ mock.method(fs, 'lstatSync', (path: string) => {
};
});

describe('Sandbox to check if region is bootstrapped', () => {
// class under test
let sandboxInstance: FileWatchingSandbox;

const cdkExecutor = new AmplifySandboxExecutor(backendDeployer);

beforeEach(async () => {
// ensures that .gitignore is set as absent
mock.method(fs, 'existsSync', () => false);
ssmClientSendMock.mock.resetCalls();
});

afterEach(async () => {
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();
});

sandboxInstance = new FileWatchingSandbox(
'testSandboxId',
clientConfigGeneratorAdapter,
cdkExecutor,
ssmClientMock,
openMock as never
);

it('region has not bootstrapped', async () => {
ssmClientSendMock.mock.mockImplementationOnce(() =>
Promise.resolve({
Parameters: [{ name: 'testParam' }],
NextToken: undefined,
})
);

await sandboxInstance.start({
dir: 'testDir',
exclude: ['exclude1', 'exclude2'],
clientConfigFilePath: path.join('test', 'location'),
format: ClientConfigFormat.JS,
});

assert.strictEqual(ssmClientSendMock.mock.callCount(), 1);
assert.strictEqual(openMock.mock.callCount(), 1);
});
});

describe('Sandbox using local project name resolver', () => {
// class under test
let sandboxInstance: FileWatchingSandbox;
Expand All @@ -65,7 +121,8 @@ describe('Sandbox using local project name resolver', () => {
sandboxInstance = new FileWatchingSandbox(
'testSandboxId',
clientConfigGeneratorAdapter,
cdkExecutor
cdkExecutor,
ssmClientMock
);
await sandboxInstance.start({
dir: 'testDir',
Expand All @@ -90,13 +147,15 @@ describe('Sandbox using local project name resolver', () => {
execaDestroyMock.mock.resetCalls();
execaDeployMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
});

afterEach(async () => {
execaDestroyMock.mock.resetCalls();
execaDeployMock.mock.resetCalls();
subscribeMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();
});

Expand Down Expand Up @@ -269,7 +328,8 @@ describe('Sandbox with user provided app name', () => {
sandboxInstance = new FileWatchingSandbox(
'testSandboxId',
clientConfigGeneratorAdapter,
cdkExecutor
cdkExecutor,
ssmClientMock
);
await sandboxInstance.start({
dir: 'testDir',
Expand All @@ -289,13 +349,15 @@ describe('Sandbox with user provided app name', () => {
execaDestroyMock.mock.resetCalls();
execaDeployMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
});

afterEach(async () => {
execaDestroyMock.mock.resetCalls();
execaDeployMock.mock.resetCalls();
subscribeMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();
});

Expand Down Expand Up @@ -393,15 +455,15 @@ describe('Sandbox with absolute output path', () => {
sandboxInstance = new FileWatchingSandbox(
'testSandboxId',
clientConfigGeneratorAdapter,
cdkExecutor
cdkExecutor,
ssmClientMock
);
await sandboxInstance.start({
dir: 'testDir',
exclude: ['exclude1', 'exclude2'],
name: 'customSandboxName',
format: ClientConfigFormat.JSON,
clientConfigFilePath: path.join('test', 'location'),
profile: 'amplify-sandbox',
});
if (
subscribeMock.mock.calls[0].arguments[1] &&
Expand All @@ -414,13 +476,15 @@ describe('Sandbox with absolute output path', () => {
execaDeployMock.mock.resetCalls();
execaDestroyMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
});

afterEach(async () => {
execaDeployMock.mock.resetCalls();
execaDestroyMock.mock.resetCalls();
subscribeMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();
});

Expand All @@ -445,10 +509,6 @@ describe('Sandbox with absolute output path', () => {
path.join('test', 'location')
);
});

it('sets AWS profile when starting sandbox', async () => {
assert.strictEqual(process.env.AWS_PROFILE, 'amplify-sandbox');
});
});

describe('Sandbox ignoring paths in .gitignore', () => {
Expand Down Expand Up @@ -478,7 +538,8 @@ describe('Sandbox ignoring paths in .gitignore', () => {
sandboxInstance = new FileWatchingSandbox(
'testSandboxId',
clientConfigGeneratorAdapter,
cdkExecutor
cdkExecutor,
ssmClientMock
);
await sandboxInstance.start({
dir: 'testDir',
Expand All @@ -498,13 +559,15 @@ describe('Sandbox ignoring paths in .gitignore', () => {
execaDeployMock.mock.resetCalls();
execaDestroyMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
});

afterEach(async () => {
execaDeployMock.mock.resetCalls();
execaDestroyMock.mock.resetCalls();
subscribeMock.mock.resetCalls();
generateClientConfigMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();
});

Expand Down
47 changes: 42 additions & 5 deletions packages/sandbox/src/file_watching_sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { ClientConfigGeneratorAdapter } from './config/client_config_generator_a
import parseGitIgnore from 'parse-gitignore';
import path from 'path';
import fs from 'fs';
import _open from 'open';
import { DescribeParametersCommand, SSMClient } from '@aws-sdk/client-ssm';

const CDK_BOOTSTRAP_PARAM_PREFIX = '/cdk-bootstrap';
// TODO: finalize bootstrap url. This is just a placeholder for now.
const AMPLIFY_CONSOLE_BOOTSTRAP_URL = `https://<REGION>.console.aws.amazon.com/amplify/create/bootstrap?region=<REGION>#/`;
/**
* Runs a file watcher and deploys
*/
Expand All @@ -21,7 +26,9 @@ export class FileWatchingSandbox implements Sandbox {
constructor(
private readonly sandboxId: string,
private readonly clientConfigGenerator: ClientConfigGeneratorAdapter,
private readonly executor: AmplifySandboxExecutor
private readonly executor: AmplifySandboxExecutor,
private readonly ssmClient: SSMClient,
private readonly open = _open
) {
process.once('SIGINT', () => void this.stop());
process.once('SIGTERM', () => void this.stop());
Expand All @@ -31,9 +38,15 @@ export class FileWatchingSandbox implements Sandbox {
* @inheritdoc
*/
start = async (options: SandboxOptions) => {
const { profile } = options;
if (profile) {
process.env.AWS_PROFILE = profile;
const bootstrapped = await this.hasBootstrapped();
if (!bootstrapped) {
console.warn(
'The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process and re-run the amplify sandbox command.'
);
// get region from an available sdk client;
const region = await this.ssmClient.config.region();
this.open(AMPLIFY_CONSOLE_BOOTSTRAP_URL.replaceAll('<REGION>', region));
return;
}

const sandboxId = options.name ?? this.sandboxId;
Expand Down Expand Up @@ -137,7 +150,8 @@ export class FileWatchingSandbox implements Sandbox {
*/
stop = async () => {
console.debug(`[Sandbox] Shutting down`);
await this.watcherSubscription.unsubscribe();
// can be undefined if command exits before subscription
await this.watcherSubscription?.unsubscribe();
};

/**
Expand Down Expand Up @@ -197,4 +211,27 @@ export class FileWatchingSandbox implements Sandbox {
}
return [];
};

/**
* Check if given region has been bootstrapped using SSM param.
* @returns A Boolean that represents if region has been bootstrapped
*/
private hasBootstrapped = async () => {
let bootstrapped = false;
let nextToken;
do {
const { Parameters, NextToken } = await this.ssmClient.send(
new DescribeParametersCommand({})
);
if (
Parameters?.some((p) => p.Name?.startsWith(CDK_BOOTSTRAP_PARAM_PREFIX))
) {
bootstrapped = true;
break;
}
nextToken = NextToken;
} while (nextToken);

return bootstrapped;
};
}
1 change: 0 additions & 1 deletion packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export type SandboxOptions = {
exclude?: string[];
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()
Expand Down
4 changes: 3 additions & 1 deletion packages/sandbox/src/sandbox_singleton_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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';
import { SSMClient } from '@aws-sdk/client-ssm';

/**
* Factory to create a new sandbox
Expand All @@ -30,7 +31,8 @@ export class SandboxSingletonFactory {
this.instance = new FileWatchingSandbox(
await this.sandboxIdResolver(),
this.clientConfigGenerator,
new AmplifySandboxExecutor(BackendDeployerFactory.getInstance())
new AmplifySandboxExecutor(BackendDeployerFactory.getInstance()),
new SSMClient()
);
}
return this.instance;
Expand Down

0 comments on commit 59a2a6e

Please sign in to comment.