Skip to content

Commit

Permalink
Add hotswap to canaries (#2289)
Browse files Browse the repository at this point in the history
* Add hotswap to canaries

* try this

* try this

* try this

* try this

* try this

* change this

* pr feedback

* pr feedback
  • Loading branch information
sobolk authored Dec 3, 2024
1 parent cfdc854 commit 1f56a5f
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .changeset/twenty-baboons-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@
"homedir",
"hotfix",
"hotswap",
"hotswappable",
"hotswapped",
"hotswapping",
"iamv2",
"identitypool",
"idps",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const waitForSandboxToBecomeIdle = () =>
'Watching for file changes...'
);

/**
* Reusable predicates: Wait for sandbox to indicate that it's executing hotswap deployment, i.e. "hotswapping resources:"
*/
export const waitForSandboxToBeginHotswappingResources = () =>
new PredicatedActionBuilder().waitForLineIncludes('hotswapping resources:');

/**
* Reusable predicated action: Wait for sandbox delete to prompt to delete all the resource and respond with yes
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
interruptSandbox,
replaceFiles,
waitForConfigUpdateAfterDeployment,
waitForSandboxToBeginHotswappingResources,
} from '../../process-controller/predicated_action_macros.js';
import { BackendIdentifier } from '@aws-amplify/plugin-types';
import { testConcurrencyLevel } from '../test_concurrency.js';
Expand Down Expand Up @@ -93,7 +94,12 @@ export const defineSandboxTest = (testProjectCreator: TestProjectCreator) => {
for (const update of updates) {
processController
.do(replaceFiles(update.replacements))
.do(ensureDeploymentTimeLessThan(update.deployThresholdSec));
.do(waitForSandboxToBeginHotswappingResources());
if (update.deployThresholdSec) {
processController.do(
ensureDeploymentTimeLessThan(update.deployThresholdSec)
);
}
}

// Execute the process.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, before, beforeEach, describe, it } from 'node:test';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import os, { userInfo } from 'os';
import { execa } from 'execa';
import { ampxCli } from '../process-controller/process_controller.js';
import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js';
Expand All @@ -14,11 +14,15 @@ import {
import {
confirmDeleteSandbox,
interruptSandbox,
replaceFiles,
waitForSandboxDeploymentToPrintTotalTime,
waitForSandboxToBeginHotswappingResources,
} from '../process-controller/predicated_action_macros.js';
import { BackendIdentifierConversions } from '@aws-amplify/platform-core';
import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js';
import { amplifyAtTag } from '../constants.js';
import { FunctionCodeHotswapTestProjectCreator } from '../test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.js';
import { BackendIdentifier } from '@aws-amplify/plugin-types';

const cfnClient = new CloudFormationClient(e2eToolingClientConfig);

Expand Down Expand Up @@ -134,4 +138,47 @@ void describe('Live dependency health checks', { concurrency: true }, () => {
.run();
});
});

void describe('sandbox hotswap', () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-amplify'));
});

afterEach(async () => {
await fs.rm(tempDir, { recursive: true });
});

void it('can hotswap function code', async () => {
const projectCreator = new FunctionCodeHotswapTestProjectCreator();
const testProject = await projectCreator.createProject(tempDir);

const sandboxBackendIdentifier: BackendIdentifier = {
type: 'sandbox',
namespace: testProject.name,
name: userInfo().username,
};

await testProject.deploy(sandboxBackendIdentifier);

const processController = ampxCli(
['sandbox', '--dirToWatch', 'amplify'],
testProject.projectDirPath
);
const updates = await testProject.getUpdates();
for (const update of updates) {
processController
.do(replaceFiles(update.replacements))
.do(waitForSandboxToBeginHotswappingResources())
.do(waitForSandboxDeploymentToPrintTotalTime());
}

// Execute the process.
await processController.do(interruptSandbox()).run();

// Clean up
await testProject.tearDown(sandboxBackendIdentifier);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Projects in this directory are meant for `live-dependency-health-checks` (aka canaries).

1. These projects must not be used in e2e tests to provide deep functional coverage.
2. These projects must be lightweight to provide fast runtime and stability.
3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import fs from 'fs/promises';
import { createEmptyAmplifyProject } from '../create_empty_amplify_project.js';
import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
import { TestProjectBase, TestProjectUpdate } from '../test_project_base.js';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'path';
import { TestProjectCreator } from '../test_project_creator.js';
import { AmplifyClient } from '@aws-sdk/client-amplify';
import { e2eToolingClientConfig } from '../../e2e_tooling_client_config.js';
import { execa } from 'execa';

/**
* Creates test projects with function hotswap.
*/
export class FunctionCodeHotswapTestProjectCreator
implements TestProjectCreator
{
readonly name = 'function-code-hotswap';

/**
* Creates project creator.
*/
constructor(
private readonly cfnClient: CloudFormationClient = new CloudFormationClient(
e2eToolingClientConfig
),
private readonly amplifyClient: AmplifyClient = new AmplifyClient(
e2eToolingClientConfig
)
) {}

createProject = async (e2eProjectDir: string): Promise<TestProjectBase> => {
const { projectName, projectRoot, projectAmplifyDir } =
await createEmptyAmplifyProject(this.name, e2eProjectDir);

const project = new FunctionCodeHotswapTestTestProject(
projectName,
projectRoot,
projectAmplifyDir,
this.cfnClient,
this.amplifyClient
);
await fs.cp(
project.sourceProjectAmplifyDirURL,
project.projectAmplifyDirPath,
{
recursive: true,
}
);

// we're not starting from create flow. install latest versions of dependencies.
await execa(
'npm',
[
'install',
'@aws-amplify/backend',
'@aws-amplify/backend-cli',
'aws-cdk@^2',
'aws-cdk-lib@^2',
'constructs@^10.0.0',
'typescript@^5.0.0',
'tsx',
'esbuild',
],
{
cwd: projectRoot,
stdio: 'inherit',
}
);

return project;
};
}

/**
* Test project with function hotswap.
*/
class FunctionCodeHotswapTestTestProject extends TestProjectBase {
// Note that this is pointing to the non-compiled project directory
// This allows us to test that we are able to deploy js, cjs, ts, etc. without compiling with tsc first
readonly sourceProjectRootPath =
'../../../src/test-projects/live-dependency-health-checks-projects/function-code-hotswap';

readonly sourceProjectRootURL: URL = new URL(
this.sourceProjectRootPath,
import.meta.url
);

readonly sourceProjectAmplifyDirURL: URL = new URL(
`${this.sourceProjectRootPath}/amplify`,
import.meta.url
);

private readonly sourceProjectUpdateDirURL: URL = new URL(
`${this.sourceProjectRootPath}/hotswap-update-files`,
import.meta.url
);

/**
* Create a test project instance.
*/
constructor(
name: string,
projectDirPath: string,
projectAmplifyDirPath: string,
cfnClient: CloudFormationClient,
amplifyClient: AmplifyClient
) {
super(
name,
projectDirPath,
projectAmplifyDirPath,
cfnClient,
amplifyClient
);
}

/**
* @inheritdoc
*/
override async getUpdates(): Promise<TestProjectUpdate[]> {
return [
{
replacements: [
this.getUpdateReplacementDefinition('func-src/handler.ts'),
],
},
];
}

private getUpdateReplacementDefinition = (suffix: string) => ({
source: this.getSourceProjectUpdatePath(suffix),
destination: this.getTestProjectPath(suffix),
});

private getSourceProjectUpdatePath = (suffix: string) =>
pathToFileURL(
path.join(fileURLToPath(this.sourceProjectUpdateDirURL), suffix)
);

private getTestProjectPath = (suffix: string) =>
pathToFileURL(path.join(this.projectAmplifyDirPath, suffix));
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type TestProjectUpdate = {
* Windows has a separate threshold because it is consistently slower than other platforms
* https://github.com/microsoft/Windows-Dev-Performance/issues/17
*/
deployThresholdSec: PlatformDeploymentThresholds;
deployThresholdSec?: PlatformDeploymentThresholds;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Projects in this directory are meant for `live-dependency-health-checks` (aka canaries).

1. These projects must not be used in e2e tests to provide deep functional coverage.
2. These projects must be lightweight to provide fast runtime and stability.
3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineBackend } from '@aws-amplify/backend';
import { nodeFunc } from './function.js';

defineBackend({ nodeFunc });
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Dummy lambda handler.
*/
export const handler = async () => {
return 'Hello';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineFunction } from '@aws-amplify/backend';

export const nodeFunc = defineFunction({
name: 'nodeFunction',
entry: './func-src/handler.ts',
timeoutSeconds: 5,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Non-functional change to the lambda, but it triggers a sandbox hotswap
*/
export const handler = async () => {
return 'Hello V2';
};

0 comments on commit 1f56a5f

Please sign in to comment.