Skip to content

Commit

Permalink
chore: separate func invoke and build
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardfoyle committed Jan 27, 2021
1 parent 525deb6 commit 33a1a02
Show file tree
Hide file tree
Showing 41 changed files with 351 additions and 371 deletions.
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 { ResourceMeta } 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 cont =
!!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 (!cont) {
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 ResourceMeta[];
};
39 changes: 19 additions & 20 deletions packages/amplify-category-function/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ 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 { $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 } 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';
Expand Down Expand Up @@ -148,25 +150,22 @@ 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<any>> {
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,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,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 ResourceMeta = ResourceTuple & {
service: string;
build: boolean;
distZipFilename: string;
lastBuildTimeStamp?: string;
lastPackageTimeStamp?: string;
skipHashing: boolean;
};

export type Packager = (context: $TSContext, resource: ResourceMeta) => Promise<{ zipFilename: string; zipFilePath: string }>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { $TSContext, pathManager } from 'amplify-cli-core';
import { FunctionRuntimeLifecycleManager, BuildRequest, BuildType } from 'amplify-function-plugin-interface';
import { ResourceMeta } from '../types/packaging-types';
import * as path from 'path';

export const buildFunction = async (context: $TSContext, resource: ResourceMeta, buildType: BuildType = BuildType.PROD) => {
const resourcePath = path.join(pathManager.getBackendDirPath(), resource.category, resource.resourceName);
const breadcrumbs = context.amplify.readBreadcrumbs(resource.category, resource.resourceName);

const runtimePlugin: FunctionRuntimeLifecycleManager = (await context.amplify.loadRuntimePlugin(
context,
breadcrumbs.pluginId,
)) as FunctionRuntimeLifecycleManager;

const depCheck = await runtimePlugin.checkDependencies(breadcrumbs.functionRuntime);
if (!depCheck.hasRequiredDependencies) {
context.print.error(depCheck.errorMessage || `You are missing dependencies required to package ${resource.resourceName}`);
throw new Error(`Missing required dependencies to package ${resource.resourceName}`);
}

const prevBuildTime = resource.lastBuildTimeStamp ? new Date(resource.lastBuildTimeStamp) : undefined;

// build the function
let rebuilt = false;
if (breadcrumbs.scripts && breadcrumbs.scripts.build) {
// TODO
throw new Error('Executing custom build scripts is not yet implemented');
} else {
const buildRequest: BuildRequest = {
buildType,
srcRoot: resourcePath,
runtime: breadcrumbs.functionRuntime,
legacyBuildHookParams: {
projectRoot: pathManager.findProjectRoot(),
resourceName: resource.resourceName,
},
lastBuildTimeStamp: prevBuildTime,
};
rebuilt = (await runtimePlugin.build(buildRequest)).rebuilt;
}
if (rebuilt) {
context.amplify.updateamplifyMetaAfterBuild(resource, buildType);
return new Date().toISOString();
} else {
return resource?.lastBuildTimeStamp;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
import { ServiceName } from './constants';
import _ from 'lodash';
import { LayerParameters } from './layerParams';
import { ResourceMeta } from '../types/packaging-types';
import { $TSContext, ResourceTuple } from 'amplify-cli-core';
import { category } from '../../../constants';
/*
* This file contains the logic for loading, selecting and executing function plugins (currently runtime and template plugins)
*/
Expand Down Expand Up @@ -205,6 +208,17 @@ export async function loadPluginFromFactory(pluginPath, expectedFactoryFunction,
return plugin[expectedFactoryFunction](context);
}

export async function getRuntimeManager(
context: $TSContext,
resourceName: string,
): Promise<FunctionRuntimeLifecycleManager & { runtime: string }> {
const { pluginId, functionRuntime } = context.amplify.readBreadcrumbs(category, resourceName);
return {
...((await context.amplify.loadRuntimePlugin(context, pluginId)) as FunctionRuntimeLifecycleManager),
runtime: functionRuntime,
};
}

// Convenience interfaces that are private to this class

interface PluginSelectionOptions<T extends FunctionRuntimeCondition | FunctionTemplateCondition> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { $TSContext } from 'amplify-cli-core';
import _ from 'lodash';
import { Packager, ResourceMeta } from '../types/packaging-types';
import { ServiceName } from './constants';
import { packageFunction } from './packageFunction';
import { packageLayer } from './packageLayer';

export const packageResource: Packager = async (context, resource) => getServicePackager(resource.service)(context, resource);

const servicePackagerMap: Record<ServiceName, Packager> = {
[ServiceName.LambdaFunction]: packageFunction,
[ServiceName.LambdaLayer]: packageLayer,
};

const getServicePackager = (service: string) =>
(servicePackagerMap[service] ??
(() => {
throw new Error(`Unknown function service type ${service}`);
})) as Packager;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// For legacy purposes, the method builds and packages the resource

import { pathManager } from 'amplify-cli-core';
import * as path from 'path';
import * as fs from 'fs-extra';
import { Packager } from '../types/packaging-types';
import { getRuntimeManager } from './functionPluginLoader';

/**
* Packages lambda source code and artifacts into a lambda-compatible .zip file
*/
export const packageFunction: Packager = async (context, resource) => {
const resourcePath = path.join(pathManager.getBackendDirPath(), resource.category, resource.resourceName);
const runtimeManager = await getRuntimeManager(context, resource.resourceName);
const distDir = path.join(resourcePath, 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir);
}
const destination = path.join(distDir, 'latest-build.zip');
const packageRequest = {
env: context.amplify.getEnvInfo().envName,
srcRoot: resourcePath,
dstFilename: destination,
runtime: runtimeManager.runtime,
lastPackageTimeStamp: resource.lastPackageTimeStamp ? new Date(resource.lastPackageTimeStamp) : undefined,
lastBuildTimeStamp: resource.lastBuildTimeStamp ? new Date(resource.lastBuildTimeStamp) : undefined,
skipHashing: resource.skipHashing,
};
const packageResult = await runtimeManager.package(packageRequest);
const packageHash = packageResult.packageHash;
const zipFilename = packageHash
? `${resource.resourceName}-${packageHash}-build.zip`
: resource.distZipFilename ?? `${resource.category}-${resource.resourceName}-build.zip`;
context.amplify.updateAmplifyMetaAfterPackage(resource, zipFilename);
return { zipFilename, zipFilePath: destination };
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { prompt } from 'inquirer';
import path from 'path';
import _ from 'lodash';
import { hashElement } from 'folder-hash';
import { pathManager } from 'amplify-cli-core';
import { FunctionDependency } from 'amplify-function-plugin-interface';
import { $TSContext, pathManager } from 'amplify-cli-core';
import { ServiceName, provider } from './constants';
import { previousPermissionsQuestion } from './layerHelpers';
import {
Expand All @@ -22,13 +21,14 @@ import { getLayerRuntimes } from './layerRuntimes';
import crypto from 'crypto';
import { updateLayerArtifacts } from './storeResources';
import globby from 'globby';
import { Packager, ResourceMeta } from '../types/packaging-types';

export async function packageLayer(context, resource: Resource) {
export const packageLayer: Packager = async (context, resource) => {
await ensureLayerVersion(context, resource.resourceName);
return zipLayer(context, resource);
}
};

async function zipLayer(context, resource: Resource) {
async function zipLayer(context: $TSContext, resource: ResourceMeta) {
const zipFilename = 'latest-build.zip';
const layerName = resource.resourceName;
const layerDirPath = path.join(pathManager.getBackendDirPath(), resource.category, layerName);
Expand All @@ -38,7 +38,7 @@ async function zipLayer(context, resource: Resource) {
const zip = archiver.create('zip');
const output = fs.createWriteStream(destination);

return new Promise((resolve, reject) => {
return new Promise<{ zipFilePath: string; zipFilename: string }>((resolve, reject) => {
output.on('close', () => {
// check zip size is less than 250MB
if (validFilesize(destination)) {
Expand Down Expand Up @@ -86,7 +86,7 @@ async function zipLayer(context, resource: Resource) {
}

// Check hash results for content changes, bump version if so
async function ensureLayerVersion(context: any, layerName: string) {
async function ensureLayerVersion(context: $TSContext, layerName: string) {
const layerState = getLayerMetadataFactory(context)(layerName);
const isNewVersion = await layerState.syncVersions();
const latestVersion = layerState.getLatestVersion();
Expand Down Expand Up @@ -165,10 +165,7 @@ export const hashLayerVersionContents = async (layerPath: string): Promise<strin

const joinedHashes = (await Promise.all([safeHash(nodePath, nodeHashOptions), safeHash(pyPath), safeHash(optPath)])).join();

return crypto
.createHash('sha256')
.update(joinedHashes)
.digest('base64');
return crypto.createHash('sha256').update(joinedHashes).digest('base64');
};

// wrapper around hashElement that will return an empty string if the path does not exist
Expand All @@ -192,10 +189,3 @@ function validFilesize(path, maxSize = 250) {
return new Error(`Calculating file size failed: ${path}`);
}
}

interface Resource {
service: ServiceName;
dependsOn?: FunctionDependency[];
resourceName: string;
category: string;
}
1 change: 1 addition & 0 deletions packages/amplify-cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/node": "^10.17.13",
"@types/rimraf": "^3.0.0",
"@types/uuid": "^8.0.0",
"amplify-function-plugin-interface": "1.4.1",
"rimraf": "^3.0.0"
},
"jest": {
Expand Down
Loading

0 comments on commit 33a1a02

Please sign in to comment.