mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
545 lines
19 KiB
JavaScript
545 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const crypto = require('crypto');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const wait = require('timers-ext/promise/sleep');
|
|
const validate = require('./lib/validate');
|
|
const filesize = require('../../utils/filesize');
|
|
const ServerlessError = require('../../serverless-error');
|
|
const { log, style, progress } = require('@serverless/utils/log');
|
|
|
|
const mainProgress = progress.get('main');
|
|
|
|
class AwsDeployFunction {
|
|
constructor(serverless, options) {
|
|
this.serverless = serverless;
|
|
this.options = options || {};
|
|
this.packagePath =
|
|
this.options.package ||
|
|
this.serverless.service.package.path ||
|
|
path.join(this.serverless.serviceDir || '.', '.serverless');
|
|
this.provider = this.serverless.getProvider('aws');
|
|
|
|
this.shouldEnsureFunctionState = false;
|
|
|
|
Object.assign(this, validate);
|
|
|
|
this.hooks = {
|
|
'initialize': () => {
|
|
const commandName = this.serverless.processedInput.commands.join(' ');
|
|
if (commandName !== 'deploy function') return;
|
|
log.notice();
|
|
log.notice(
|
|
`Deploying function ${this.options.function} to stage ${this.serverless
|
|
.getProvider('aws')
|
|
.getStage()} ${style.aside(`(${this.serverless.getProvider('aws').getRegion()})`)}`
|
|
);
|
|
log.info();
|
|
},
|
|
'before:deploy:function:initialize': () =>
|
|
mainProgress.notice('Validating', { isMainEvent: true }),
|
|
'deploy:function:initialize': async () => {
|
|
await this.validate();
|
|
await this.checkIfFunctionExists();
|
|
this.checkIfFunctionChangesBetweenImageAndHandler();
|
|
},
|
|
|
|
'before:deploy:function:packageFunction': () =>
|
|
mainProgress.notice('Retrieving function info', { isMainEvent: true }),
|
|
'deploy:function:packageFunction': async () =>
|
|
this.serverless.pluginManager.spawn('package:function'),
|
|
|
|
'before:deploy:function:deploy': () =>
|
|
mainProgress.notice('Packaging', { isMainEvent: true }),
|
|
'deploy:function:deploy': async () => {
|
|
if (!this.options['update-config']) {
|
|
await this.deployFunction();
|
|
}
|
|
await this.updateFunctionConfiguration();
|
|
if (this.shouldEnsureFunctionState) {
|
|
await this.ensureFunctionState();
|
|
}
|
|
await this.serverless.pluginManager.spawn('aws:common:cleanupTempDir');
|
|
},
|
|
};
|
|
}
|
|
|
|
async checkIfFunctionExists() {
|
|
// check if the function exists in the service
|
|
this.options.functionObj = this.serverless.service.getFunction(this.options.function);
|
|
|
|
// check if function exists on AWS
|
|
const params = {
|
|
FunctionName: this.options.functionObj.name,
|
|
};
|
|
|
|
const result = await (async () => {
|
|
try {
|
|
return await this.provider.request('Lambda', 'getFunction', params);
|
|
} catch (error) {
|
|
if (_.get(error, 'providerError.code') === 'ResourceNotFoundException') {
|
|
const errorMessage = [
|
|
`The function "${this.options.function}" you want to update is not yet deployed.`,
|
|
' Please run "serverless deploy" to deploy your service.',
|
|
' After that you can redeploy your services functions with the',
|
|
' "serverless deploy function" command.',
|
|
].join('');
|
|
throw new ServerlessError(errorMessage, 'FUNCTION_NOT_YET_DEPLOYED');
|
|
}
|
|
throw error;
|
|
}
|
|
})();
|
|
|
|
if (result) this.serverless.service.provider.remoteFunctionData = result;
|
|
}
|
|
|
|
checkIfFunctionChangesBetweenImageAndHandler() {
|
|
const functionObject = this.serverless.service.getFunction(this.options.function);
|
|
const remoteFunctionPackageType =
|
|
this.serverless.service.provider.remoteFunctionData.Configuration.PackageType;
|
|
|
|
if (functionObject.handler && remoteFunctionPackageType === 'Image') {
|
|
throw new ServerlessError(
|
|
`The function "${this.options.function}" you want to update with handler was previously packaged as an image. Please run "serverless deploy" to ensure consistent deploy.`,
|
|
'DEPLOY_FUNCTION_CHANGE_BETWEEN_HANDLER_AND_IMAGE_ERROR'
|
|
);
|
|
}
|
|
|
|
if (functionObject.image && remoteFunctionPackageType === 'Zip') {
|
|
throw new ServerlessError(
|
|
`The function "${this.options.function}" you want to update with image was previously packaged as zip file. Please run "serverless deploy" to ensure consistent deploy.`,
|
|
'DEPLOY_FUNCTION_CHANGE_BETWEEN_HANDLER_AND_IMAGE_ERROR'
|
|
);
|
|
}
|
|
}
|
|
|
|
async normalizeArnRole(role) {
|
|
if (typeof role === 'string') {
|
|
if (role.indexOf(':') !== -1) {
|
|
return role;
|
|
}
|
|
|
|
const roleResource = this.serverless.service.resources.Resources[role];
|
|
|
|
if (roleResource.Type !== 'AWS::IAM::Role') {
|
|
throw new ServerlessError(
|
|
'Provided resource is not IAM Role',
|
|
'ROLE_REFERENCES_NON_AWS_IAM_ROLE'
|
|
);
|
|
}
|
|
const roleProperties = roleResource.Properties;
|
|
if (!roleProperties.RoleName) {
|
|
throw new ServerlessError(
|
|
'Role resource missing RoleName property',
|
|
'MISSING_ROLENAME_FOR_ROLE'
|
|
);
|
|
}
|
|
const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`;
|
|
|
|
const result = await this.provider.getAccountInfo();
|
|
return `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`;
|
|
}
|
|
|
|
const data = await this.provider.request('IAM', 'getRole', {
|
|
RoleName: role['Fn::GetAtt'][0],
|
|
});
|
|
return data.Arn;
|
|
}
|
|
|
|
async ensureFunctionState() {
|
|
this.options.functionObj = this.serverless.service.getFunction(this.options.function);
|
|
const params = {
|
|
FunctionName: this.options.functionObj.name,
|
|
};
|
|
const startTime = Date.now();
|
|
|
|
const callWithRetry = async () => {
|
|
const result = await this.provider.request('Lambda', 'getFunction', params);
|
|
if (
|
|
result &&
|
|
result.Configuration.State === 'Active' &&
|
|
result.Configuration.LastUpdateStatus === 'Successful'
|
|
) {
|
|
return;
|
|
}
|
|
const didOneMinutePass = Date.now() - startTime > 60 * 1000;
|
|
if (didOneMinutePass) {
|
|
throw new ServerlessError(
|
|
'Ensuring function state timed out. Please try to deploy your function once again.',
|
|
'DEPLOY_FUNCTION_ENSURE_STATE_TIMED_OUT'
|
|
);
|
|
}
|
|
log.info(`Retrying ensure function state for function: ${this.options.function}.`);
|
|
await wait(500);
|
|
await callWithRetry();
|
|
};
|
|
|
|
await callWithRetry();
|
|
}
|
|
|
|
async callUpdateFunctionConfiguration(params) {
|
|
const startTime = Date.now();
|
|
|
|
const callWithRetry = async () => {
|
|
try {
|
|
await this.provider.request('Lambda', 'updateFunctionConfiguration', params);
|
|
} catch (err) {
|
|
const didOneMinutePass = Date.now() - startTime > 60 * 1000;
|
|
|
|
if (err.providerError && err.providerError.code === 'ResourceConflictException') {
|
|
if (didOneMinutePass) {
|
|
throw new ServerlessError(
|
|
'Retry timed out. Please try to deploy your function once again.',
|
|
'DEPLOY_FUNCTION_CONFIGURATION_UPDATE_TIMED_OUT'
|
|
);
|
|
}
|
|
log.info(
|
|
`Retrying configuration update for function: ${this.options.function}. Reason: ${err.message}`
|
|
);
|
|
await wait(1000);
|
|
await callWithRetry();
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
await callWithRetry();
|
|
}
|
|
|
|
async updateFunctionConfiguration() {
|
|
const functionObj = this.options.functionObj;
|
|
const providerObj = this.serverless.service.provider;
|
|
const remoteFunctionConfiguration =
|
|
this.serverless.service.provider.remoteFunctionData.Configuration;
|
|
const params = {
|
|
FunctionName: functionObj.name,
|
|
};
|
|
|
|
const kmsKeyArn = functionObj.kmsKeyArn || providerObj.kmsKeyArn;
|
|
|
|
if (kmsKeyArn) {
|
|
params.KMSKeyArn = kmsKeyArn;
|
|
}
|
|
|
|
if (params.KMSKeyArn && params.KMSKeyArn === remoteFunctionConfiguration.KMSKeyArn) {
|
|
delete params.KMSKeyArn;
|
|
}
|
|
|
|
if (functionObj.snapStart) {
|
|
params.SnapStart = {
|
|
ApplyOn: 'PublishedVersions',
|
|
};
|
|
}
|
|
|
|
if (
|
|
functionObj.description &&
|
|
functionObj.description !== remoteFunctionConfiguration.Description
|
|
) {
|
|
params.Description = functionObj.description;
|
|
}
|
|
|
|
if (functionObj.handler && functionObj.handler !== remoteFunctionConfiguration.Handler) {
|
|
params.Handler = functionObj.handler;
|
|
}
|
|
|
|
if (functionObj.memorySize) {
|
|
params.MemorySize = functionObj.memorySize;
|
|
} else if (providerObj.memorySize) {
|
|
params.MemorySize = providerObj.memorySize;
|
|
}
|
|
|
|
if (params.MemorySize && params.MemorySize === remoteFunctionConfiguration.MemorySize) {
|
|
delete params.MemorySize;
|
|
}
|
|
|
|
if (functionObj.timeout) {
|
|
params.Timeout = functionObj.timeout;
|
|
} else if (providerObj.timeout) {
|
|
params.Timeout = providerObj.timeout;
|
|
}
|
|
|
|
if (params.Timeout && params.Timeout === remoteFunctionConfiguration.Timeout) {
|
|
delete params.Timeout;
|
|
}
|
|
|
|
// Check if we have remotely managed layers and add them to the update call
|
|
// if they exist in the remote function configuration.
|
|
const isConsoleSdkLayerArn = RegExp.prototype.test.bind(
|
|
/(?:177335420605|321667558080):layer:sls-/u
|
|
);
|
|
const serverlessConsoleLayerArns = (remoteFunctionConfiguration.Layers || [])
|
|
.filter(({ Arn: arn }) => isConsoleSdkLayerArn(arn))
|
|
.map(({ Arn }) => Arn);
|
|
const hasServerlessConsoleLayers = serverlessConsoleLayerArns.length > 0;
|
|
if (!functionObj.layers || !functionObj.layers.some(_.isObject)) {
|
|
// We need to initialize to an empty array so if a layer is removed
|
|
// we will send an empty Layers array in the update call to remove any layers.
|
|
// If there are no layers in the remove config this property will be set to undefined anyway.
|
|
params.Layers = functionObj.layers || providerObj.layers || [];
|
|
|
|
if (!remoteFunctionConfiguration.Layers) {
|
|
remoteFunctionConfiguration.Layers = [];
|
|
}
|
|
|
|
if (hasServerlessConsoleLayers) {
|
|
for (const layer of serverlessConsoleLayerArns) {
|
|
if (!params.Layers.includes(layer)) {
|
|
params.Layers.push(layer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Do not attach layers to the update call if the layers did not change.
|
|
if (
|
|
params.Layers &&
|
|
remoteFunctionConfiguration.Layers &&
|
|
_.isEqual(
|
|
new Set(params.Layers),
|
|
new Set(remoteFunctionConfiguration.Layers.map((layer) => layer.Arn))
|
|
)
|
|
) {
|
|
delete params.Layers;
|
|
}
|
|
}
|
|
|
|
if (
|
|
functionObj.onError &&
|
|
!_.isObject(functionObj.onError) &&
|
|
_.get(remoteFunctionConfiguration, 'DeadLetterConfig.TargetArn', null) !== functionObj.onError
|
|
) {
|
|
params.DeadLetterConfig = {
|
|
TargetArn: functionObj.onError,
|
|
};
|
|
}
|
|
|
|
// Add empty environment object if it does not exist
|
|
// so when we do the comparison below it will be equal to an empty object
|
|
params.Environment = {
|
|
Variables: {},
|
|
};
|
|
if (!remoteFunctionConfiguration.Environment) {
|
|
remoteFunctionConfiguration.Environment = {
|
|
Variables: {},
|
|
};
|
|
}
|
|
|
|
if (functionObj.environment || providerObj.environment) {
|
|
params.Environment.Variables = Object.assign(
|
|
{},
|
|
providerObj.environment,
|
|
functionObj.environment
|
|
);
|
|
}
|
|
if (Object.values(params.Environment.Variables).some((value) => _.isObject(value))) {
|
|
delete params.Environment;
|
|
} else {
|
|
Object.keys(params.Environment.Variables).forEach((key) => {
|
|
// taken from the bash man pages
|
|
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
|
|
const errorMessage = 'Invalid characters in environment variable';
|
|
throw new ServerlessError(errorMessage, 'DEPLOY_FUNCTION_INVALID_ENV_VARIABLE');
|
|
}
|
|
|
|
if (params.Environment.Variables[key] != null) {
|
|
params.Environment.Variables[key] = String(params.Environment.Variables[key]);
|
|
}
|
|
});
|
|
}
|
|
// If we detected remotely managed layers, we need to add the environment variables
|
|
// that are managed by the Serverless Console to the update call so they do not get removed.
|
|
if (params.Environment && hasServerlessConsoleLayers) {
|
|
const consoleEnvironmentVariableNames = [
|
|
'AWS_LAMBDA_EXEC_WRAPPER',
|
|
'SLS_ORG_ID',
|
|
'SLS_DEV_MODE_ORG_ID',
|
|
'SLS_DEV_TOKEN',
|
|
'SERVERLESS_PLATFORM_STAGE',
|
|
];
|
|
const remoteVariables = remoteFunctionConfiguration.Environment.Variables;
|
|
const localVariables = params.Environment.Variables;
|
|
for (const variableName of consoleEnvironmentVariableNames) {
|
|
if (remoteVariables[variableName] && !localVariables[variableName]) {
|
|
localVariables[variableName] = remoteVariables[variableName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
params.Environment &&
|
|
remoteFunctionConfiguration.Environment &&
|
|
_.isEqual(params.Environment.Variables, remoteFunctionConfiguration.Environment.Variables)
|
|
) {
|
|
delete params.Environment;
|
|
}
|
|
|
|
if (functionObj.vpc || providerObj.vpc) {
|
|
const vpc = functionObj.vpc || providerObj.vpc;
|
|
params.VpcConfig = {};
|
|
|
|
if (Array.isArray(vpc.securityGroupIds) && !vpc.securityGroupIds.some(_.isObject)) {
|
|
params.VpcConfig.SecurityGroupIds = vpc.securityGroupIds;
|
|
}
|
|
|
|
if (Array.isArray(vpc.subnetIds) && !vpc.subnetIds.some(_.isObject)) {
|
|
params.VpcConfig.SubnetIds = vpc.subnetIds;
|
|
}
|
|
|
|
const didVpcChange = () => {
|
|
const remoteConfigToCompare = { SecurityGroupIds: [], SubnetIds: [] };
|
|
if (remoteFunctionConfiguration.VpcConfig) {
|
|
remoteConfigToCompare.SecurityGroupIds = new Set(
|
|
remoteFunctionConfiguration.VpcConfig.SecurityGroupIds || []
|
|
);
|
|
remoteConfigToCompare.SubnetIds = new Set(
|
|
remoteFunctionConfiguration.VpcConfig.SubnetIds || []
|
|
);
|
|
}
|
|
const localConfigToCompare = {
|
|
SecurityGroupIds: new Set(params.VpcConfig.SecurityGroupIds || []),
|
|
SubnetIds: new Set(params.VpcConfig.SubnetIds || []),
|
|
};
|
|
return _.isEqual(remoteConfigToCompare, localConfigToCompare);
|
|
};
|
|
|
|
if (!Object.keys(params.VpcConfig).length || didVpcChange()) {
|
|
delete params.VpcConfig;
|
|
}
|
|
}
|
|
|
|
const executionRole = this.provider.getCustomExecutionRole(functionObj);
|
|
if (executionRole) {
|
|
params.Role = await this.normalizeArnRole(executionRole);
|
|
}
|
|
|
|
if (params.Role === remoteFunctionConfiguration.Role) {
|
|
delete params.Role;
|
|
}
|
|
|
|
if (functionObj.image) {
|
|
const imageConfig = {};
|
|
if (_.isObject(functionObj.image)) {
|
|
if (functionObj.image.command) {
|
|
imageConfig.Command = functionObj.image.command;
|
|
}
|
|
if (functionObj.image.entryPoint) {
|
|
imageConfig.EntryPoint = functionObj.image.entryPoint;
|
|
}
|
|
if (functionObj.image.workingDirectory) {
|
|
imageConfig.WorkingDirectory = functionObj.image.workingDirectory;
|
|
}
|
|
}
|
|
|
|
if (
|
|
!_.isEqual(
|
|
imageConfig,
|
|
_.get(remoteFunctionConfiguration, 'ImageConfigResponse.ImageConfig', {})
|
|
)
|
|
) {
|
|
params.ImageConfig = imageConfig;
|
|
}
|
|
}
|
|
|
|
if (!Object.keys(_.omit(params, 'FunctionName')).length) {
|
|
if (this.options['update-config']) log.notice();
|
|
const noticeMessage = [
|
|
'Function configuration did not change, and the update was skipped.',
|
|
' If you made changes to the service configuration and expected them to be deployed,',
|
|
' it most likely means that they can only be applied with a full service deployment.',
|
|
].join('');
|
|
log.notice.skip(
|
|
`${noticeMessage} ${style.aside(
|
|
`(${Math.floor(
|
|
(Date.now() - this.serverless.pluginManager.commandRunStartTime) / 1000
|
|
)}s)`
|
|
)}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
mainProgress.notice('Updating function configuration', { isMainEvent: true });
|
|
|
|
await this.callUpdateFunctionConfiguration(params);
|
|
this.shouldEnsureFunctionState = true;
|
|
if (this.options['update-config']) log.notice();
|
|
log.notice.success(
|
|
`Function configuration updated ${style.aside(
|
|
`(${Math.floor((Date.now() - this.serverless.pluginManager.commandRunStartTime) / 1000)}s)`
|
|
)}\n`
|
|
);
|
|
}
|
|
|
|
async deployFunction() {
|
|
const functionObject = this.serverless.service.getFunction(this.options.function);
|
|
const params = {
|
|
FunctionName: this.options.functionObj.name,
|
|
};
|
|
|
|
if (functionObject.image) {
|
|
const { functionImageUri, functionImageSha } = await this.provider.resolveImageUriAndSha(
|
|
this.options.function
|
|
);
|
|
const remoteImageSha =
|
|
this.serverless.service.provider.remoteFunctionData.Configuration.CodeSha256;
|
|
if (remoteImageSha === functionImageSha && !this.options.force) {
|
|
log.notice();
|
|
log.notice.skip(
|
|
`Image did not change. Function deployment skipped. ${style.aside(
|
|
`(${Math.floor(
|
|
(Date.now() - this.serverless.pluginManager.commandRunStartTime) / 1000
|
|
)}s)`
|
|
)}`
|
|
);
|
|
return;
|
|
}
|
|
params.ImageUri = functionImageUri;
|
|
} else {
|
|
const artifactFileName = this.provider.naming.getFunctionArtifactName(this.options.function);
|
|
let artifactFilePath =
|
|
this.serverless.service.package.artifact || path.join(this.packagePath, artifactFileName);
|
|
// check if an artifact is used in function package level
|
|
if (_.get(functionObject, 'package.artifact')) {
|
|
artifactFilePath = functionObject.package.artifact;
|
|
}
|
|
|
|
const data = fs.readFileSync(artifactFilePath);
|
|
|
|
const remoteHash =
|
|
this.serverless.service.provider.remoteFunctionData.Configuration.CodeSha256;
|
|
const localHash = crypto.createHash('sha256').update(data).digest('base64');
|
|
|
|
if (remoteHash === localHash && !this.options.force) {
|
|
log.notice();
|
|
log.notice.skip(
|
|
`Code did not change. Function deployment skipped. ${style.aside(
|
|
`(${Math.floor(
|
|
(Date.now() - this.serverless.pluginManager.commandRunStartTime) / 1000
|
|
)}s)`
|
|
)}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
params.ZipFile = data;
|
|
|
|
const stats = fs.statSync(artifactFilePath);
|
|
mainProgress.notice(`Uploading ${style.aside(`(${filesize(stats.size)})`)}`, {
|
|
isMainEvent: true,
|
|
});
|
|
}
|
|
|
|
mainProgress.notice('Deploying', { isMainEvent: true });
|
|
await this.provider.request('Lambda', 'updateFunctionCode', params);
|
|
this.shouldEnsureFunctionState = true;
|
|
log.notice();
|
|
log.notice.success(
|
|
`Function code deployed ${style.aside(
|
|
`(${Math.floor((Date.now() - this.serverless.pluginManager.commandRunStartTime) / 1000)}s)`
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = AwsDeployFunction;
|