import getS3EndpointForRegion from '../utils/get-s3-endpoint-for-region.js' import utils from '@serverlessinc/sf-core/src/utils.js' import isChangeSetWithoutChanges from '../utils/is-change-set-without-changes.js' const { log, progress } = utils const NO_UPDATE_MESSAGE = 'No updates are to be performed.' export default { async createFallback() { this.createLater = false progress .get('main') .notice('Creating AWS CloudFormation stack', { isMainEvent: true }) const stackName = this.provider.naming.getStackName() const compiledTemplateFileName = this.provider.naming.getCompiledTemplateS3Suffix() const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion()) const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}` let monitorCfData if ( !this.serverless.service.provider.deploymentMethod || this.serverless.service.provider.deploymentMethod === 'direct' ) { const params = this.getCreateStackParams({ templateUrl, }) monitorCfData = await this.provider.request( 'CloudFormation', 'createStack', params, ) } else { // Change-set based deployment const changeSetName = this.provider.naming.getStackChangeSetName() const createChangeSetParams = this.getCreateChangeSetParams({ changeSetType: 'CREATE', templateUrl, }) const executeChangeSetParams = this.getExecuteChangeSetParams() // Create new change set this.provider.didCreateService = true log.info('Creating new change set') await this.provider.request( 'CloudFormation', 'createChangeSet', createChangeSetParams, ) // Wait for changeset to be created log.info('Waiting for new change set to be created') const changeSetDescription = await this.waitForChangeSetCreation( changeSetName, stackName, ) // Check if stack has changes if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it') await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }) this.serverless.service.provider.deploymentWithEmptyChangeSet = true return false } log.info('Executing created change set') await this.provider.request( 'CloudFormation', 'executeChangeSet', executeChangeSetParams, ) monitorCfData = changeSetDescription } await this.monitorStack('create', monitorCfData) return true }, async update() { const compiledTemplateFileName = this.provider.naming.getCompiledTemplateS3Suffix() const s3Endpoint = getS3EndpointForRegion(this.provider.getRegion()) const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}` const stackName = this.provider.naming.getStackName() let monitorCfData if ( !this.serverless.service.provider.deploymentMethod || this.serverless.service.provider.deploymentMethod === 'direct' ) { const params = this.getUpdateStackParams({ templateUrl }) try { monitorCfData = await this.provider.request( 'CloudFormation', 'updateStack', params, ) } catch (e) { if (e.message.includes(NO_UPDATE_MESSAGE)) { return false } throw e } } else { // Change-set based deployment const changeSetName = this.provider.naming.getStackChangeSetName() const createChangeSetParams = this.getCreateChangeSetParams({ changeSetType: 'UPDATE', templateUrl, }) const executeChangeSetParams = this.getExecuteChangeSetParams() // Ensure that previous change set has been removed await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }) // Create new change set log.info('Creating new change set') await this.provider.request( 'CloudFormation', 'createChangeSet', createChangeSetParams, ) // Wait for changeset to be created log.info('Waiting for new change set to be created') const changeSetDescription = await this.waitForChangeSetCreation( changeSetName, stackName, ) // Check if stack has changes if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it') await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }) this.serverless.service.provider.deploymentWithEmptyChangeSet = true return false } log.info('Executing created change set') await this.provider.request( 'CloudFormation', 'executeChangeSet', executeChangeSetParams, ) monitorCfData = changeSetDescription } await this.monitorStack('update', monitorCfData) // Policy must have at least one statement, otherwise no updates would be possible at all // Stack policy must be set after change set has been executed, // as it might reference resources newly added in that change set. // Applied only for ChangeSet deployments which is a default method if ( this.serverless.service.provider.deploymentMethod === 'changesets' && this.serverless.service.provider.stackPolicy && Object.keys(this.serverless.service.provider.stackPolicy).length ) { log.info('Setting stack policy') const stackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }) await this.provider.request('CloudFormation', 'setStackPolicy', { StackName: stackName, StackPolicyBody: stackPolicyBody, }) } return true }, async updateStack() { return Promise.resolve(this).then(() => { if (this.createLater) { return Promise.resolve(this).then(() => this.createFallback()) } return Promise.resolve(this).then(() => this.update()) }) }, }