in packages/amplify-provider-awscloudformation/src/push-resources.ts [865:1163]
export async function formNestedStack(
context: $TSContext,
projectDetails: $TSObject,
categoryName?: string,
resourceName?: string,
serviceName?: string,
skipEnv?: boolean,
useExistingMeta?: boolean
) {
let rootStack;
// CFN transform for Root stack
rootStack = await transformRootStack(context);
// get the {deploymentBucketName , AuthRoleName , UnAuthRole from overridded data}
const metaToBeUpdated = {
DeploymentBucketName: rootStack.Resources.DeploymentBucket.Properties.BucketName,
AuthRoleName: rootStack.Resources.AuthRole.Properties.RoleName,
UnauthRoleName: rootStack.Resources.UnauthRole.Properties.RoleName,
};
// sanitize this data if needed
for (const key of Object.keys(metaToBeUpdated)) {
if (typeof metaToBeUpdated[key] === 'object' && 'Ref' in metaToBeUpdated[key]) {
delete metaToBeUpdated[key];
}
}
const projectPath = pathManager.findProjectRoot();
const amplifyMeta = useExistingMeta ? projectDetails.amplifyMeta : stateManager.getMeta(projectPath);
// update amplify meta with updated root stack Info
if (Object.keys(metaToBeUpdated).length) {
context.amplify.updateProvideramplifyMeta(providerName, metaToBeUpdated);
//update teamProviderInfo
const { envName } = context.amplify.getEnvInfo();
const teamProviderInfo = stateManager.getTeamProviderInfo(projectPath);
const tpiResourceParams: $TSAny = _.get(teamProviderInfo, [envName, 'awscloudformation'], {});
_.assign(tpiResourceParams, metaToBeUpdated);
_.set(teamProviderInfo, [envName, 'awscloudformation'], tpiResourceParams);
stateManager.setTeamProviderInfo(projectPath, teamProviderInfo);
}
// Track Amplify Console generated stacks
try {
const appId = amplifyMeta.providers[providerName].AmplifyAppId;
if ((await isAmplifyAdminApp(appId)).isAdminApp) {
rootStack.Description = 'Root Stack for AWS Amplify Console';
}
} catch (err) {
// if it is not an AmplifyAdmin app, do nothing
}
let authResourceName: string;
const { APIGatewayAuthURL, NetworkStackS3Url, AuthTriggerTemplateURL } = amplifyMeta.providers[constants.ProviderName];
const { envName } = stateManager.getLocalEnvInfo(projectPath);
if (APIGatewayAuthURL) {
const stack = {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: APIGatewayAuthURL,
Parameters: {
authRoleName: {
Ref: 'AuthRoleName',
},
unauthRoleName: {
Ref: 'UnauthRoleName',
},
env: envName,
},
},
};
const apis: $TSObject = amplifyMeta?.api ?? {};
for (const [apiName, api] of Object.entries(apis)) {
if (await loadApiCliInputs(context, apiName, api)) {
stack.Properties.Parameters[apiName] = {
'Fn::GetAtt': [api.providerMetadata.logicalId, 'Outputs.ApiId'],
};
}
}
rootStack.Resources[APIGW_AUTH_STACK_LOGICAL_ID] = stack;
}
if (AuthTriggerTemplateURL) {
const stack = {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: AuthTriggerTemplateURL,
Parameters: {
env: envName,
},
},
DependsOn: [],
};
const cognitoResource = stateManager.getResourceFromMeta(amplifyMeta, 'auth', 'Cognito');
const authRootStackResourceName = `auth${cognitoResource.resourceName}`;
stack.Properties.Parameters['userpoolId'] = {
'Fn::GetAtt': [authRootStackResourceName, 'Outputs.UserPoolId'],
};
stack.Properties.Parameters['userpoolArn'] = {
'Fn::GetAtt': [authRootStackResourceName, 'Outputs.UserPoolArn'],
};
stack.DependsOn.push(authRootStackResourceName);
const { dependsOn } = cognitoResource.resource as { dependsOn };
dependsOn.forEach(resource => {
const dependsOnStackName = `${resource.category}${resource.resourceName}`;
stack.DependsOn.push(dependsOnStackName);
const dependsOnAttributes = resource?.attributes;
dependsOnAttributes.forEach(attribute => {
const parameterKey = `${resource.category}${resource.resourceName}${attribute}`;
const parameterValue = { 'Fn::GetAtt': [dependsOnStackName, `Outputs.${attribute}`] };
stack.Properties.Parameters[parameterKey] = parameterValue;
});
});
rootStack.Resources[AUTH_TRIGGER_STACK] = stack;
}
if (NetworkStackS3Url) {
rootStack.Resources[NETWORK_STACK_LOGICAL_ID] = {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: NetworkStackS3Url,
},
};
rootStack.Resources.DeploymentBucket.Properties['VersioningConfiguration'] = {
Status: 'Enabled',
};
rootStack.Resources.DeploymentBucket.Properties['LifecycleConfiguration'] = {
Rules: [
{
ExpirationInDays: 7,
NoncurrentVersionExpirationInDays: 7,
Prefix: 'codepipeline-amplify/',
Status: 'Enabled',
},
],
};
}
let categories = Object.keys(amplifyMeta);
categories = categories.filter(category => category !== 'providers');
categories.forEach(category => {
const resources = Object.keys(amplifyMeta[category]);
resources.forEach(resource => {
const resourceDetails = amplifyMeta[category][resource];
if (category === 'auth' && resource !== 'userPoolGroups') {
authResourceName = resource;
}
const resourceKey = category + resource;
let templateURL;
if (resourceDetails.providerPlugin) {
const parameters = <$TSObject>loadResourceParameters(context, category, resource);
const { dependsOn } = resourceDetails;
if (dependsOn) {
for (let i = 0; i < dependsOn.length; ++i) {
for (const attribute of dependsOn[i]?.attributes || []) {
// If the depends on resource is an imported resource we cannot form GetAtt type reference
// since there is no such thing. We have to read the output.{AttributeName} from the meta
// and inject the value itself into the parameters block
let parameterValue;
const dependentResource = _.get(amplifyMeta, [dependsOn[i].category, dependsOn[i].resourceName], undefined);
if (!dependentResource && dependsOn[i].category) {
throw new Error(`Cannot get resource: ${dependsOn[i].resourceName} from '${dependsOn[i].category}' category.`);
}
if (dependentResource && dependentResource.serviceType === 'imported') {
const outputAttributeValue = _.get(dependentResource, ['output', attribute], undefined);
if (!outputAttributeValue) {
const error = new Error(
`Cannot read the '${attribute}' dependent attribute value from the output section of resource: '${dependsOn[i].resourceName}'.`,
);
error.stack = undefined;
throw error;
}
parameterValue = outputAttributeValue;
} else {
// Fn::GetAtt adds dependency in root stack and dependsOn stack
const dependsOnStackName = dependsOn[i].category + dependsOn[i].resourceName;
parameterValue = { 'Fn::GetAtt': [dependsOnStackName, `Outputs.${attribute}`] };
}
const parameterKey = `${dependsOn[i].category}${dependsOn[i].resourceName}${attribute}`;
if (!isAuthTrigger(dependsOn[i])) {
parameters[parameterKey] = parameterValue;
}
}
if (dependsOn[i].exports) {
Object.keys(dependsOn[i].exports)
.map(key => ({ key, value: dependsOn[i].exports[key] }))
.forEach(({ key, value }) => {
parameters[key] = { 'Fn::ImportValue': value };
});
}
}
}
for (const [key, value] of Object.entries(parameters)) {
if (Array.isArray(value)) {
parameters[key] = value.join();
}
}
if (
(category === AmplifyCategories.API || category === AmplifyCategories.HOSTING) &&
resourceDetails.service === ApiServiceNameElasticContainer
) {
parameters['deploymentBucketName'] = Fn.Ref('DeploymentBucketName');
parameters['rootStackName'] = Fn.Ref('AWS::StackName');
}
const currentEnv = context.amplify.getEnvInfo().envName;
if (!skipEnv && resourceName) {
if (resource === resourceName && category === categoryName && amplifyMeta[category][resource].service === serviceName) {
Object.assign(parameters, { env: currentEnv });
}
} else if (!skipEnv) {
Object.assign(parameters, { env: currentEnv });
}
// If auth is imported check the parameters section of the nested template
// and if it has auth or unauth role arn or name or userpool id, then inject it from the
// imported auth resource's properties
const { imported, userPoolId, authRoleArn, authRoleName, unauthRoleArn, unauthRoleName } =
context.amplify.getImportedAuthProperties(context);
if (category !== AmplifyCategories.AUTH && resourceDetails.service !== 'Cognito' && imported) {
if (parameters.AuthCognitoUserPoolId) {
parameters.AuthCognitoUserPoolId = userPoolId;
}
if (parameters.authRoleArn) {
parameters.authRoleArn = authRoleArn;
}
if (parameters.authRoleName) {
parameters.authRoleName = authRoleName || { Ref: 'AuthRoleName' }; // if only a user pool is imported, we ref the root stack AuthRoleName because the child stacks still need this parameter
}
if (parameters.unauthRoleArn) {
parameters.unauthRoleArn = unauthRoleArn;
}
if (parameters.unauthRoleName) {
parameters.unauthRoleName = unauthRoleName;
}
}
if (resourceDetails.providerMetadata) {
templateURL = resourceDetails.providerMetadata.s3TemplateURL;
rootStack.Resources[resourceKey] = {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: templateURL,
Parameters: parameters,
},
};
}
}
});
});
if (authResourceName) {
const importedAuth = _.get(amplifyMeta, [AmplifyCategories.AUTH, authResourceName], undefined);
// If auth is imported we cannot update the IDP as it is not part of the stack resources we deploy.
if (importedAuth && importedAuth.serviceType !== 'imported') {
const authParameters = loadResourceParameters(context, AmplifyCategories.AUTH, authResourceName);
if (authParameters.identityPoolName) {
updateIdPRolesInNestedStack(rootStack, authResourceName);
}
}
}
return rootStack;
}