in packages/amplify-provider-awscloudformation/src/push-resources.ts [82:441]
export async function run(context: $TSContext, resourceDefinition: $TSObject, rebuild: boolean = false) {
const deploymentStateManager = await DeploymentStateManager.createDeploymentStateManager(context);
let iterativeDeploymentWasInvoked = false;
let layerResources = [];
try {
const {
resourcesToBeCreated,
resourcesToBeUpdated,
resourcesToBeSynced,
resourcesToBeDeleted,
tagsUpdated,
allResources,
rootStackUpdated,
} = resourceDefinition;
const cloudformationMeta = context.amplify.getProjectMeta().providers.awscloudformation;
const {
parameters: { options },
} = context;
let resources = !!context?.exeInfo?.forcePush || rebuild ? allResources : resourcesToBeCreated.concat(resourcesToBeUpdated);
layerResources = resources.filter(r => r.service === AmplifySupportedService.LAMBDA_LAYER);
if (deploymentStateManager.isDeploymentInProgress() && !deploymentStateManager.isDeploymentFinished()) {
if (context.exeInfo?.forcePush || context.exeInfo?.iterativeRollback) {
await runIterativeRollback(context, cloudformationMeta, deploymentStateManager);
if (context.exeInfo?.iterativeRollback) {
return;
}
}
}
await createEnvLevelConstructs(context);
// removing dependent functions if @model{Table} is deleted
const apiResourceTobeUpdated = resourcesToBeUpdated.filter(resource => resource.service === 'AppSync');
if (apiResourceTobeUpdated.length) {
const functionResourceToBeUpdated = await ensureValidFunctionModelDependencies(
context,
apiResourceTobeUpdated,
allResources as $TSObject[],
);
// filter updated function to replace with existing updated ones(in case of duplicates)
if (functionResourceToBeUpdated !== undefined && functionResourceToBeUpdated.length > 0) {
resources = _.uniqBy(resources.concat(functionResourceToBeUpdated), `resourceName`);
}
}
validateCfnTemplates(context, resources);
for (const resource of resources) {
if (resource.service === ApiServiceNameElasticContainer && resource.category === 'api') {
const {
exposedContainer,
pipelineInfo: { consoleUrl },
} = await context.amplify.invokePluginMethod(context, 'api', undefined, 'generateContainersArtifacts', [context, resource]);
await context.amplify.updateamplifyMetaAfterResourceUpdate('api', resource.resourceName, 'exposedContainer', exposedContainer);
context.print.info(`\nIn a few moments, you can check image build status for ${resource.resourceName} at the following URL:`);
context.print.info(`${consoleUrl}\n`);
context.print.info(
`It may take a few moments for this to appear. If you have trouble with first time deployments, please try refreshing this page after a few moments and watch the CodeBuild Details for debugging information.`,
);
if (resourcesToBeUpdated.find(res => res.resourceName === resource.resourceName)) {
resource.lastPackageTimeStamp = undefined;
await context.amplify.updateamplifyMetaAfterResourceUpdate('api', resource.resourceName, 'lastPackageTimeStamp', undefined);
}
}
if (resource.service === ApiServiceNameElasticContainer && resource.category === 'hosting') {
await context.amplify.invokePluginMethod(context, 'hosting', 'ElasticContainer', 'generateHostingResources', [context, resource]);
}
}
for (const resource of layerResources) {
await legacyLayerMigration(context, resource.resourceName);
}
/**
* calling transform schema here to support old project with out overrides
*/
await transformGraphQLSchema(context, {
handleMigration: opts => updateStackForAPIMigration(context, 'api', undefined, opts),
minify: options['minify'],
promptApiKeyCreation: true,
});
await prePushLambdaLayerPrompt(context, resources);
await prepareBuildableResources(context, resources);
await buildOverridesEnabledResources(context);
//Removed api transformation to generate resources befoe starting deploy/
// If there is a deployment already in progress we have to fail the push operation as another
// push in between could lead non-recoverable stacks and files.
if (deploymentStateManager.isDeploymentInProgress()) {
deploymentInProgressErrorMessage(context);
return;
}
let deploymentSteps: DeploymentStep[] = [];
// location where the intermediate deployment steps are stored
let stateFolder: { local?: string; cloud?: string } = {};
// Check if iterative updates are enabled or not and generate the required deployment steps if needed.
if (FeatureFlags.getBoolean('graphQLTransformer.enableIterativeGSIUpdates')) {
const gqlResource = getGqlUpdatedResource(rebuild ? resources : resourcesToBeUpdated);
if (gqlResource) {
const gqlManager = await GraphQLResourceManager.createInstance(context, gqlResource, cloudformationMeta.StackId, rebuild);
deploymentSteps = await gqlManager.run();
// If any models are being replaced, we prepend steps to the iterative deployment to remove references to the replaced table in functions that have a dependeny on the tables
const modelsBeingReplaced = gqlManager.getTablesBeingReplaced().map(meta => meta.stackName); // stackName is the same as the model name
deploymentSteps = await prependDeploymentStepsToDisconnectFunctionsFromReplacedModelTables(
context,
modelsBeingReplaced,
deploymentSteps,
);
if (deploymentSteps.length > 0) {
iterativeDeploymentWasInvoked = true;
// Initialize deployment state to signal a new iterative deployment
// When using iterative push, the deployment steps provided by GraphQLResourceManager does not include the last step
// where the root stack is pushed
const deploymentStepStates: DeploymentStepState[] = new Array(deploymentSteps.length + 1).fill(true).map(() => ({
status: DeploymentStepStatus.WAITING_FOR_DEPLOYMENT,
}));
// If start cannot update because a deployment has started between the start of this method and this point
// we have to return before uploading any artifacts that could fail the other deployment.
if (!(await deploymentStateManager.startDeployment(deploymentStepStates))) {
deploymentInProgressErrorMessage(context);
return;
}
}
stateFolder.local = gqlManager.getStateFilesDirectory();
stateFolder.cloud = await gqlManager.getCloudStateFilesDirectory();
}
}
await uploadAppSyncFiles(context, resources, allResources);
await prePushAuthTransform(context, resources);
await prePushGraphQLCodegen(context, resourcesToBeCreated, resourcesToBeUpdated);
const projectDetails = context.amplify.getProjectDetails();
await prePushTemplateDescriptionHandler(context, resourcesToBeCreated);
await updateS3Templates(context, resources, projectDetails.amplifyMeta);
// We do not need CloudFormation update if only syncable resources are the changes.
if (
resourcesToBeCreated.length > 0 ||
resourcesToBeUpdated.length > 0 ||
resourcesToBeDeleted.length > 0 ||
tagsUpdated ||
rootStackUpdated ||
context.exeInfo.forcePush ||
rebuild
) {
// if there are deploymentSteps, need to do an iterative update
if (deploymentSteps.length > 0) {
// create deployment manager
const deploymentManager = await DeploymentManager.createInstance(context, cloudformationMeta.DeploymentBucketName, spinner, {
userAgent: formUserAgentParam(context, generateUserAgentAction(resourcesToBeCreated, resourcesToBeUpdated)),
});
deploymentSteps.forEach(step => deploymentManager.addStep(step));
// generate nested stack
const backEndDir = pathManager.getBackendDirPath();
const rootStackFilepath = path.normalize(path.join(backEndDir, providerName, rootStackFileName));
await generateAndUploadRootStack(context, rootStackFilepath, rootStackFileName);
// Use state manager to do the final deployment. The final deployment include not just API change but the whole Amplify Project
const finalStep: DeploymentOp = {
stackTemplatePathOrUrl: rootStackFileName,
tableNames: [],
stackName: cloudformationMeta.StackName,
parameters: {
DeploymentBucketName: cloudformationMeta.DeploymentBucketName,
AuthRoleName: cloudformationMeta.AuthRoleName,
UnauthRoleName: cloudformationMeta.UnauthRoleName,
},
capabilities: ['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'],
};
deploymentManager.addStep({
deployment: finalStep,
rollback: deploymentSteps[deploymentSteps.length - 1].deployment,
});
spinner.start();
await deploymentManager.deploy(deploymentStateManager);
// delete the intermidiate states as it is ephemeral
if (stateFolder.local) {
try {
fs.removeSync(stateFolder.local);
} catch (err) {
context.print.error(`Could not delete state directory locally: ${err}`);
}
}
const s3 = await S3.getInstance(context);
if (stateFolder.cloud) {
await s3.deleteDirectory(cloudformationMeta.DeploymentBucketName, stateFolder.cloud);
}
postDeploymentCleanup(s3, cloudformationMeta.DeploymentBucketName);
} else {
// Non iterative update
spinner.start();
const nestedStack = await formNestedStack(context, context.amplify.getProjectDetails());
try {
await updateCloudFormationNestedStack(context, nestedStack, resourcesToBeCreated, resourcesToBeUpdated);
await storeRootStackTemplate(context, nestedStack);
// if the only root stack updates, function is called with empty resources . this fn copies amplifyMeta and backend Config to #current-cloud-backend
context.amplify.updateamplifyMetaAfterPush([]);
} catch (err) {
if (err?.name === 'ValidationError' && err?.message === 'No updates are to be performed.') {
return;
} else {
throw err;
}
} finally {
spinner.stop();
}
}
}
await postPushGraphQLCodegen(context);
await amplifyServiceManager.postPushCheck(context);
if (resources.concat(resourcesToBeDeleted).length > 0) {
await context.amplify.updateamplifyMetaAfterPush(resources);
}
if (resourcesToBeSynced.length > 0) {
const importResources = resourcesToBeSynced.filter((r: { sync: string }) => r.sync === 'import');
const unlinkedResources = resourcesToBeSynced.filter((r: { sync: string }) => r.sync === 'unlink');
if (importResources.length > 0) {
await context.amplify.updateamplifyMetaAfterPush(importResources);
}
if (unlinkedResources.length > 0) {
// Sync backend-config.json to cloud folder
await context.amplify.updateamplifyMetaAfterPush(unlinkedResources);
for (let i = 0; i < unlinkedResources.length; i++) {
context.amplify.updateamplifyMetaAfterResourceDelete(unlinkedResources[i].category, unlinkedResources[i].resourceName);
}
}
}
for (let i = 0; i < resourcesToBeDeleted.length; i++) {
context.amplify.updateamplifyMetaAfterResourceDelete(resourcesToBeDeleted[i].category, resourcesToBeDeleted[i].resourceName);
}
await uploadAuthTriggerFiles(context, resourcesToBeCreated, resourcesToBeUpdated);
let updatedAllResources = (await context.amplify.getResourceStatus()).allResources;
const newAPIresources = [];
updatedAllResources = updatedAllResources.filter((resource: { service: string }) => resource.service === AmplifySupportedService.APIGW);
for (let i = 0; i < updatedAllResources.length; i++) {
if (resources.findIndex(resource => resource.resourceName === updatedAllResources[i].resourceName) > -1) {
newAPIresources.push(updatedAllResources[i]);
}
}
// Check if there was any imported auth resource and if there was we have to refresh the
// COGNITO_USER_POOLS configuration for AppSync APIs in meta if we have any
if (resourcesToBeSynced.length > 0) {
const importResources = resourcesToBeSynced.filter((r: { sync: string }) => r.sync === 'import');
if (importResources.length > 0) {
const { imported, userPoolId } = context.amplify.getImportedAuthProperties(context);
// Sanity check it will always be true in this case
if (imported) {
const appSyncAPIs = allResources.filter((resource: { service: string }) => resource.service === 'AppSync');
const meta = stateManager.getMeta(undefined);
let hasChanges = false;
for (const appSyncAPI of appSyncAPIs) {
const apiResource = _.get(meta, ['api', appSyncAPI.resourceName]);
if (apiResource) {
const defaultAuthentication = _.get(apiResource, ['output', 'authConfig', 'defaultAuthentication']);
if (defaultAuthentication && defaultAuthentication.authenticationType === 'AMAZON_COGNITO_USER_POOLS') {
defaultAuthentication.userPoolConfig.userPoolId = userPoolId;
hasChanges = true;
}
const additionalAuthenticationProviders = _.get(apiResource, ['output', 'authConfig', 'additionalAuthenticationProviders']);
for (const additionalAuthenticationProvider of additionalAuthenticationProviders) {
if (
additionalAuthenticationProvider &&
additionalAuthenticationProvider.authenticationType === 'AMAZON_COGNITO_USER_POOLS'
) {
additionalAuthenticationProvider.userPoolConfig.userPoolId = userPoolId;
hasChanges = true;
}
}
}
}
if (hasChanges) {
stateManager.setMeta(undefined, meta);
}
}
}
}
await downloadAPIModels(context, newAPIresources);
// remove emphemeral Lambda layer state
if (resources.concat(resourcesToBeDeleted).filter(r => r.service === AmplifySupportedService.LAMBDA_LAYER).length > 0) {
await postPushLambdaLayerCleanup(context, resources, projectDetails.localEnvInfo.envName);
await context.amplify.updateamplifyMetaAfterPush(resources);
}
// Store current cloud backend in S3 deployment bcuket
await storeCurrentCloudBackend(context);
await amplifyServiceManager.storeArtifactsForAmplifyService(context);
//check for auth resources and remove deployment secret for push
resources
.filter(resource => resource.category === 'auth' && resource.service === 'Cognito' && resource.providerPlugin === 'awscloudformation')
.map(({ category, resourceName }) => context.amplify.removeDeploymentSecrets(context, category, resourceName));
await adminModelgen(context, resources);
spinner.succeed('All resources are updated in the cloud');
await displayHelpfulURLs(context, resources);
} catch (error) {
if (iterativeDeploymentWasInvoked) {
await deploymentStateManager.failDeployment();
}
if (!(await canAutoResolveGraphQLAuthError(error.message))) {
spinner.fail('An error occurred when pushing the resources to the cloud');
}
rollbackLambdaLayers(layerResources);
logger('run', [resourceDefinition])(error);
throw error;
}
}