Skip to content

Commit

Permalink
feat: Separate prod and dev lambda function builds (#6494)
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardfoyle authored Feb 12, 2021
1 parent 209c4ff commit 2977c6a
Show file tree
Hide file tree
Showing 53 changed files with 740 additions and 446 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ module.exports = {
'/packages/amplify-cli-logger/lib',
'/packages/amplify-codegen-appsync-model-plugin/lib',
'/packages/amplify-e2e-core/lib',
'/packages/amplify-function-plugin-interface/lib',
'/packages/amplify-graphql-docs-generator/lib',
'/packages/amplify-graphql-types-generator/lib',
'/packages/amplify-headless-interface/lib',
Expand All @@ -317,7 +318,7 @@ module.exports = {
'/packages/graphql-transformer-*/lib',
'/packages/amplify-headless-interface/lib',
'/packages/amplify-util-headless-input/lib',
'packages/amplify-graphql-*transformer*/lib',
'/packages/amplify-graphql-*transformer*/lib',
'/packages/amplify-provider-awscloudformation/lib',
'/packages/amplify-console-integration-tests/lib',

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { openConsole, isMockable } from '../../../provider-utils/awscloudformation';
import { ServiceName } from '../../../provider-utils/awscloudformation/utils/constants';
import { open } from 'amplify-cli-core';
import { open, $TSContext, stateManager } from 'amplify-cli-core';
import { buildFunction } from '../../../provider-utils/awscloudformation/utils/buildFunction';
import { getBuilder } from '../../..';
import { BuildType } from 'amplify-function-plugin-interface';

jest.mock('amplify-cli-core');
const stateManager_mock = stateManager as jest.Mocked<typeof stateManager>;
stateManager_mock.getMeta.mockReturnValue({
function: {
testFunc: {
lastBuildTimeStamp: 'lastBuildTimeStamp',
lastDevBuildTimeStamp: 'lastDevBuildTimeStamp',
},
},
});
jest.mock('open');

jest.mock('../../../provider-utils/awscloudformation/utils/buildFunction', () => ({
buildFunction: jest.fn(),
buildTypeKeyMap: {
PROD: 'lastBuildTimeStamp',
DEV: 'lastDevBuildTimeStamp',
},
}));
const buildFunction_mock = buildFunction as jest.MockedFunction<typeof buildFunction>;

describe('awscloudformation function provider', () => {
beforeEach(() => jest.clearAllMocks());
it('opens the correct service console', () => {
const contextStub = {
amplify: {
Expand Down Expand Up @@ -38,4 +61,16 @@ describe('awscloudformation function provider', () => {
const isFunctionMockable = isMockable(ServiceName.LambdaFunction);
expect(isFunctionMockable.isMockable).toBe(true);
});

it('passes correct build timestamp to buildFunction', async () => {
const prodBuilder = await getBuilder({} as $TSContext, 'testFunc', BuildType.PROD);
await prodBuilder();
expect(buildFunction_mock.mock.calls[0][1].lastBuildTimeStamp).toEqual('lastBuildTimeStamp');

buildFunction_mock.mockClear();

const devBuilder = await getBuilder({} as $TSContext, 'testFunc', BuildType.DEV);
await devBuilder();
expect(buildFunction_mock.mock.calls[0][1].lastBuildTimeStamp).toEqual('lastDevBuildTimeStamp');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { $TSContext, pathManager } from 'amplify-cli-core';
import { BuildType, FunctionRuntimeLifecycleManager } from 'amplify-function-plugin-interface';
import { buildFunction } from '../../../..';

jest.mock('amplify-cli-core');

const pathManager_mock = pathManager as jest.Mocked<typeof pathManager>;
pathManager_mock.getBackendDirPath.mockReturnValue('mockpath');
describe('build function', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const runtimePlugin_stub = ({
checkDependencies: jest.fn().mockResolvedValue({ hasRequiredDependencies: true }),
build: jest.fn().mockResolvedValue({ rebuilt: true }),
} as unknown) as jest.Mocked<FunctionRuntimeLifecycleManager>;

const context_stub = ({
amplify: {
readBreadcrumbs: jest.fn().mockReturnValue({ pluginId: 'testPluginId' }),
loadRuntimePlugin: jest.fn().mockResolvedValue(runtimePlugin_stub),
updateamplifyMetaAfterBuild: jest.fn(),
},
} as unknown) as jest.Mocked<$TSContext>;
it('delegates dependency checks to the runtime manager before building', async () => {
let depCheck = false;
runtimePlugin_stub.checkDependencies.mockImplementationOnce(async () => {
depCheck = true;
return {
hasRequiredDependencies: true,
};
});

runtimePlugin_stub.build.mockImplementationOnce(async () => {
if (!depCheck) {
throw new Error('Dep check not called before build');
}
return {
rebuilt: true,
};
});

await buildFunction(context_stub, { resourceName: 'testFunc' });

expect(runtimePlugin_stub.checkDependencies.mock.calls.length).toBe(1);
expect(runtimePlugin_stub.build.mock.calls.length).toBe(1);
});

it('updates amplify meta after prod', async () => {
await buildFunction(context_stub, { resourceName: 'testFunc' });

expect((context_stub.amplify.updateamplifyMetaAfterBuild as jest.Mock).mock.calls[0]).toEqual([
{ category: 'function', resourceName: 'testFunc' },
'PROD',
]);
});

it('updates amplify meta after dev build', async () => {
await buildFunction(context_stub, { resourceName: 'testFunc', buildType: BuildType.DEV });

expect((context_stub.amplify.updateamplifyMetaAfterBuild as jest.Mock).mock.calls[0]).toEqual([
{ category: 'function', resourceName: 'testFunc' },
'DEV',
]);
});

it('doesnt update amplify meta if function not rebuilt', async () => {
runtimePlugin_stub.build.mockResolvedValueOnce({ rebuilt: false });

await buildFunction(context_stub, { resourceName: 'testFunc' });

expect((context_stub.amplify.updateamplifyMetaAfterBuild as jest.Mock).mock.calls.length).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getRuntimeManager } from '../../../../provider-utils/awscloudformation/utils/functionPluginLoader';
import { $TSContext, pathManager } from 'amplify-cli-core';
import { FunctionRuntimeLifecycleManager } from 'amplify-function-plugin-interface';
import { packageFunction } from '../../../../provider-utils/awscloudformation/utils/packageFunction';
import { PackageRequestMeta } from '../../../../provider-utils/awscloudformation/types/packaging-types';

jest.mock('fs-extra');
jest.mock('amplify-cli-core');
jest.mock('../../../../provider-utils/awscloudformation/utils/functionPluginLoader');

const context_stub = {
amplify: {
getEnvInfo: jest.fn().mockReturnValue('mockEnv'),
updateAmplifyMetaAfterPackage: jest.fn(),
},
};

const pathManager_mock = pathManager as jest.Mocked<typeof pathManager>;
const getRuntimeManager_mock = getRuntimeManager as jest.MockedFunction<typeof getRuntimeManager>;

pathManager_mock.getBackendDirPath.mockReturnValue('backend/dir/path');

const runtimeManager_mock = {
package: jest.fn().mockResolvedValue({
packageHash: 'testpackagehash',
}),
};
getRuntimeManager_mock.mockResolvedValue(runtimeManager_mock as any);

const resourceRequest: PackageRequestMeta = {
category: 'testcategory',
resourceName: 'testResourceName',
service: 'testservice',
build: true,
lastPackageTimeStamp: 'lastpackagetime',
lastBuildTimeStamp: 'lastbuildtime',
distZipFilename: 'testzipfile',
skipHashing: false,
};

describe('package function', () => {
it('delegates packaging to the runtime manager', async () => {
await packageFunction((context_stub as unknown) as $TSContext, resourceRequest);

expect(runtimeManager_mock.package.mock.calls[0][0].srcRoot).toEqual('backend/dir/path/testcategory/testResourceName');
});

it('updates amplify meta after packaging', async () => {
await packageFunction((context_stub as unknown) as $TSContext, resourceRequest);

expect(context_stub.amplify.updateAmplifyMetaAfterPackage.mock.calls[0][0]).toEqual(resourceRequest);
});
});
53 changes: 39 additions & 14 deletions packages/amplify-category-function/src/commands/function/build.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import { category as categoryName } from '../../constants';
import { $TSContext } from 'amplify-cli-core';
import { ServiceName } from '../..';
import { category } from '../../constants';
import { PackageRequestMeta } from '../../provider-utils/awscloudformation/types/packaging-types';
import { buildFunction } from '../../provider-utils/awscloudformation/utils/buildFunction';
import { packageResource } from '../../provider-utils/awscloudformation/utils/package';

const subcommand = 'build';
export const name = 'build';

module.exports = {
name: subcommand,
run: async context => {
const { amplify, parameters } = context;
const resourceName = parameters.first;
/**
* To maintain existing behavior, this function builds and then packages lambda functions
*/
export const run = async (context: $TSContext) => {
const resourceName = context?.input?.subCommands?.[0];
const confirmContinue =
!!resourceName ||
context.input?.options?.yes ||
(await context.amplify.confirmPrompt(
'This will build all functions and layers in your project. Are you sure you want to continue?',
false,
));
if (!confirmContinue) {
return;
}
try {
const resourcesToBuild = (await getSelectedResources(context, resourceName))
.filter(resource => resource.build)
.filter(resource => resource.service === ServiceName.LambdaFunction);
for await (const resource of resourcesToBuild) {
resource.lastBuildTimeStamp = await buildFunction(context, resource);
await packageResource(context, resource);
}
} catch (err) {
context.print.info(err.stack);
context.print.error('There was an error building the function resources');
context.usageData.emitError(err);
process.exitCode = 1;
}
};

return amplify.buildResources(context, categoryName, resourceName).catch(err => {
context.print.info(err.stack);
context.print.error('There was an error building the function resources');
context.usageData.emitError(err);
process.exitCode = 1;
});
},
const getSelectedResources = async (context: $TSContext, resourceName?: string) => {
return (await context.amplify.getResourceStatus(category, resourceName)).allResources as PackageRequestMeta[];
};
49 changes: 28 additions & 21 deletions packages/amplify-category-function/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import path from 'path';
import { category } from './constants';
export { category } from './constants';
import { FunctionBreadcrumbs, FunctionRuntimeLifecycleManager } from 'amplify-function-plugin-interface';
import { stateManager } from 'amplify-cli-core';
import { BuildType, FunctionBreadcrumbs, FunctionRuntimeLifecycleManager } from 'amplify-function-plugin-interface';
import { $TSAny, $TSContext, pathManager, stateManager } from 'amplify-cli-core';
import sequential from 'promise-sequential';
import { updateConfigOnEnvInit } from './provider-utils/awscloudformation';
import { supportedServices } from './provider-utils/supported-services';
import _ from 'lodash';
export { packageLayer, hashLayerResource } from './provider-utils/awscloudformation/utils/packageLayer';
export { buildFunction, buildTypeKeyMap } from './provider-utils/awscloudformation/utils/buildFunction';
export { packageResource } from './provider-utils/awscloudformation/utils/package';
export { hashLayerResource } from './provider-utils/awscloudformation/utils/packageLayer';
import { ServiceName } from './provider-utils/awscloudformation/utils/constants';
export { ServiceName } from './provider-utils/awscloudformation/utils/constants';
import { isMultiEnvLayer } from './provider-utils/awscloudformation/utils/layerParams';
import { buildFunction, buildTypeKeyMap } from './provider-utils/awscloudformation/utils/buildFunction';
export { isMultiEnvLayer } from './provider-utils/awscloudformation/utils/layerParams';

export { askExecRolePermissionsQuestions } from './provider-utils/awscloudformation/service-walkthroughs/execPermissionsWalkthrough';
Expand Down Expand Up @@ -155,28 +158,32 @@ export async function initEnv(context) {
await sequential(functionTasks);
}

// returns a function that can be used to invoke the lambda locally
export async function getInvoker(context: any, params: InvokerParameters): Promise<({ event: any }) => Promise<any>> {
const resourcePath = path.join(context.amplify.pathManager.getBackendDirPath(), category, params.resourceName);
const breadcrumbs: FunctionBreadcrumbs = context.amplify.readBreadcrumbs(context, category, params.resourceName);
const runtimeManager: FunctionRuntimeLifecycleManager = await context.amplify.loadRuntimePlugin(context, breadcrumbs.pluginId);

const lastBuildTimestampStr = (await context.amplify.getResourceStatus(category, params.resourceName)).allResources.find(
resource => resource.resourceName === params.resourceName,
).lastBuildTimeStamp as string;

return async request =>
await runtimeManager.invoke({
handler: params.handler,
event: JSON.stringify(request.event),
env: context.amplify.getEnvInfo().envName,
runtime: breadcrumbs.functionRuntime,
// Returns a wrapper around FunctionRuntimeLifecycleManager.invoke() that can be used to invoke the function with only an event
export async function getInvoker(
context: $TSContext,
{ handler, resourceName, envVars }: InvokerParameters,
): Promise<({ event: unknown }) => Promise<$TSAny>> {
const resourcePath = path.join(pathManager.getBackendDirPath(), category, resourceName);
const { pluginId, functionRuntime }: FunctionBreadcrumbs = context.amplify.readBreadcrumbs(category, resourceName);
const runtimeManager: FunctionRuntimeLifecycleManager = await context.amplify.loadRuntimePlugin(context, pluginId);

return ({ event }) =>
runtimeManager.invoke({
handler: handler,
event: JSON.stringify(event),
runtime: functionRuntime,
srcRoot: resourcePath,
envVars: params.envVars,
lastBuildTimestamp: lastBuildTimestampStr ? new Date(lastBuildTimestampStr) : undefined,
envVars: envVars,
});
}

export function getBuilder(context: $TSContext, resourceName: string, buildType: BuildType): () => Promise<void> {
const lastBuildTimeStamp = _.get(stateManager.getMeta(), [category, resourceName, buildTypeKeyMap[buildType]]);
return async () => {
await buildFunction(context, { resourceName, buildType, lastBuildTimeStamp });
};
}

export function isMockable(context: any, resourceName: string): IsMockableResponse {
const resourceValue = _.get(context.amplify.getProjectMeta(), [category, resourceName]);
if (!resourceValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export async function updateFunctionResource(context, category, service, paramet
}

if (!parameters || (parameters && !parameters.skipEdit)) {
const breadcrumb = context.amplify.readBreadcrumbs(context, categoryName, parameters.resourceName);
const breadcrumb = context.amplify.readBreadcrumbs(categoryName, parameters.resourceName);
const displayName = 'trigger' in parameters ? parameters.resourceName : undefined;
await openEditor(context, category, parameters.resourceName, { defaultEditorFile: breadcrumb.defaultEditorFile }, displayName, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export async function updateWalkthrough(context, lambdaToUpdate?: string) {
const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath();
const resourceDirPath = path.join(projectBackendDirPath, category, functionParameters.resourceName);
const currentParameters = loadFunctionParameters(context, resourceDirPath);
const functionRuntime = context.amplify.readBreadcrumbs(context, category, functionParameters.resourceName).functionRuntime as string;
const functionRuntime = context.amplify.readBreadcrumbs(category, functionParameters.resourceName).functionRuntime as string;

const cfnParameters: any = JSONUtilities.readJson(path.join(resourceDirPath, parametersFileName), { throwIfNotExist: false }) || {};
const scheduleParameters = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { $TSContext, ResourceTuple } from 'amplify-cli-core';

export type PackageRequestMeta = ResourceTuple & {
service: string;
build: boolean;
distZipFilename: string;
lastBuildTimeStamp?: string;
lastPackageTimeStamp?: string;
skipHashing: boolean;
};

export type Packager = (context: $TSContext, resource: PackageRequestMeta) => Promise<{ zipFilename: string; zipFilePath: string }>;
Loading

0 comments on commit 2977c6a

Please sign in to comment.