Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hotswap to canaries #2289

Merged
merged 9 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Fixed Show fixed Hide fixed
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';
Fixed Show fixed Hide fixed

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';
};
Loading