UNPKG

@aws-amplify/amplify-category-api

Version:
335 lines (290 loc) 12.2 kB
import * as path from 'path'; import fs from 'fs-extra'; import rimraf from 'rimraf'; import { $TSContext, AmplifyCategories, CloudformationProviderFacade, JSONUtilities, pathManager, stateManager, } from '@aws-amplify/amplify-cli-core'; import { CloudFormation, Fn } from 'cloudform-types'; import { ResourceConstants } from 'graphql-transformer-common'; import { pullAllBy, find } from 'lodash'; import { printer } from '@aws-amplify/amplify-prompts'; import { DeploymentResources } from './cdk-compat/deployment-resources'; import { TransformerProjectConfig } from './cdk-compat/project-config'; const PARAMETERS_FILE_NAME = 'parameters.json'; const CUSTOM_ROLES_FILE_NAME = 'custom-roles.json'; const AMPLIFY_ADMIN_ROLE = '_Full-access/CognitoIdentityCredentials'; const AMPLIFY_MANAGE_ROLE = '_Manage-only/CognitoIdentityCredentials'; const PROVIDER_NAME = 'awscloudformation'; interface CustomRolesConfig { adminRoleNames?: Array<string> | string; } export const getIdentityPoolId = async (ctx: $TSContext): Promise<string | undefined> => { const { allResources, resourcesToBeDeleted } = await ctx.amplify.getResourceStatus('auth'); const authResources = pullAllBy(allResources, resourcesToBeDeleted, 'resourceName'); const authResource = find(authResources, { service: 'Cognito', providerPlugin: PROVIDER_NAME }) as any; return authResource?.output?.IdentityPoolId; }; export const getAdminRoles = async (ctx: $TSContext, apiResourceName: string | undefined): Promise<Array<string>> => { let currentEnv; const adminRoles = new Array<string>(); try { currentEnv = ctx.amplify.getEnvInfo().envName; } catch (err) { // When there is no environment info, return [] - This is required for sandbox pull return []; } // admin ui roles try { const amplifyMeta = stateManager.getMeta(); const appId = amplifyMeta?.providers?.[PROVIDER_NAME]?.AmplifyAppId; const res = await CloudformationProviderFacade.isAmplifyAdminApp(ctx, appId); if (res.userPoolID) { adminRoles.push(`${res.userPoolID}${AMPLIFY_ADMIN_ROLE}`, `${res.userPoolID}${AMPLIFY_MANAGE_ROLE}`); } } catch (err) { // no need to error if not admin ui app } // additonal admin role checks if (apiResourceName) { // lambda functions which have access to the api const { allResources, resourcesToBeDeleted } = await ctx.amplify.getResourceStatus('function'); const resources = pullAllBy(allResources, resourcesToBeDeleted, 'resourceName') .filter((r: any) => r.dependsOn?.some((d: any) => d?.resourceName === apiResourceName)) .map((r: any) => `${r.resourceName}-${currentEnv}`); adminRoles.push(...resources); // check for custom iam admin roles const customRoleFile = path.join( pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.API, apiResourceName), CUSTOM_ROLES_FILE_NAME, ); if (fs.existsSync(customRoleFile)) { const customRoleConfig = JSONUtilities.readJson<CustomRolesConfig>(customRoleFile); if (customRoleConfig && customRoleConfig.adminRoleNames) { const customAdminRoles = Array.isArray(customRoleConfig.adminRoleNames) ? customRoleConfig.adminRoleNames : [customRoleConfig.adminRoleNames]; const adminRoleNames = customAdminRoles // eslint-disable-next-line no-template-curly-in-string .map((r) => (r.includes('${env}') ? r.replace('${env}', currentEnv) : r)); adminRoles.push(...adminRoleNames); } } } return adminRoles; }; export function mergeUserConfigWithTransformOutput( userConfig: TransformerProjectConfig, transformOutput: DeploymentResources, opts?: any, ): DeploymentResources { const userFunctions = userConfig.functions || {}; const userResolvers = userConfig.resolvers || {}; const userPipelineFunctions = userConfig.pipelineFunctions || {}; const { functions } = transformOutput; const { resolvers } = transformOutput; const { pipelineFunctions } = transformOutput; if (!opts?.disableFunctionOverrides) { for (const userFunction of Object.keys(userFunctions)) { functions[userFunction] = userFunctions[userFunction]; } } if (!opts?.disablePipelineFunctionOverrides) { const pipelineFunctionKeys = Object.keys(userPipelineFunctions); if (pipelineFunctionKeys.length > 0) { printer.warn( ' You are using the "pipelineFunctions" directory for overridden and custom resolvers. ' + 'Please use the "resolvers" directory as "pipelineFunctions" will be deprecated.\n', ); } for (const userPipelineFunction of pipelineFunctionKeys) resolvers[userPipelineFunction] = userPipelineFunctions[userPipelineFunction]; } if (!opts?.disableResolverOverrides) { for (const userResolver of Object.keys(userResolvers)) { if (userResolver !== 'README.md') { resolvers[userResolver] = userResolvers[userResolver].toString(); } } } const stacks = overrideUserDefinedStacks(userConfig, transformOutput); return { ...transformOutput, functions, resolvers, pipelineFunctions, stacks, }; } function overrideUserDefinedStacks(userConfig: TransformerProjectConfig, transformOutput: DeploymentResources) { const userStacks = userConfig.stacks || {}; const { stacks, rootStack } = transformOutput; const resourceTypesToDependOn = { 'AWS::CloudFormation::Stack': true, 'AWS::AppSync::GraphQLApi': true, 'AWS::AppSync::GraphQLSchema': true, }; const allResourceIds = Object.keys(rootStack.Resources).filter((k: string) => { const resource = rootStack.Resources[k]; return resourceTypesToDependOn[resource.Type]; }); const parametersKeys = Object.keys(rootStack.Parameters); const customStackParams = parametersKeys.reduce( (acc: any, k: string) => ({ ...acc, [k]: Fn.Ref(k), }), {}, ); customStackParams[ResourceConstants.PARAMETERS.AppSyncApiId] = Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'); const updatedParameters = rootStack.Parameters; for (const userStack of Object.keys(userStacks)) { if (stacks[userStack]) { throw new Error(`You cannot provide a stack named ${userStack} as it \ will be overwritten by a stack generated by the GraphQL Transform.`); } const userDefinedStack = userStacks[userStack]; for (const key of Object.keys(userDefinedStack.Parameters)) { if (customStackParams[key] == null) { customStackParams[key] = Fn.Ref(key); if (updatedParameters[key]) throw new Error(`Cannot redefine CloudFormation parameter ${key} in stack ${userStack}.`); else updatedParameters[key] = userDefinedStack.Parameters[key]; } } const parametersForStack = Object.keys(userDefinedStack.Parameters).reduce( (acc, k) => ({ ...acc, [k]: customStackParams[k], }), {}, ); stacks[userStack] = userDefinedStack; const stackResourceId = userStack.split(/[^A-Za-z]/).join(''); const customNestedStack = new CloudFormation.Stack({ Parameters: parametersForStack, TemplateURL: Fn.Join('/', [ 'https://s3.amazonaws.com', Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), 'stacks', userStack, ]), }).dependsOn(allResourceIds); rootStack.Resources[stackResourceId] = customNestedStack; } rootStack.Parameters = updatedParameters; return stacks; } /** * Writes a deployment to disk at a path. */ export async function writeDeploymentToDisk( context: $TSContext, deployment: DeploymentResources, directory: string, rootStackFileName = 'rootStack.json', buildParameters: Object, ) { fs.ensureDirSync(directory); // Delete the last deployments resources except for tsconfig if present emptyBuildDirPreserveTsconfig(directory); // Write the schema to disk const { schema } = deployment; const fullSchemaPath = path.normalize(`${directory}/schema.graphql`); fs.writeFileSync(fullSchemaPath, schema); // Setup the directories if they do not exist. initStacksAndResolversDirectories(directory); // Write resolvers to disk const resolverFileNames = Object.keys(deployment.resolvers); const resolverRootPath = resolverDirectoryPath(directory); for (const resolverFileName of resolverFileNames) { const fullResolverPath = path.normalize(`${resolverRootPath}/${resolverFileName}`); fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]); } // Write pipeline resolvers to disk const pipelineFunctions = Object.keys(deployment.pipelineFunctions); const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory); for (const functionFileName of pipelineFunctions) { const fullTemplatePath = path.normalize(`${pipelineFunctionRootPath}/${functionFileName}`); fs.writeFileSync(fullTemplatePath, deployment.pipelineFunctions[functionFileName]); } // Write the stacks to disk const stackNames = Object.keys(deployment.stacks); const stackRootPath = stacksDirectoryPath(directory); for (const stackFileName of stackNames) { const fileNameParts = stackFileName.split('.'); if (fileNameParts.length === 1) { fileNameParts.push('json'); } const fullFileName = fileNameParts.join('.'); throwIfNotJSONExt(fullFileName); const fullStackPath = path.normalize(`${stackRootPath}/${fullFileName}`); let stackContent = deployment.stacks[stackFileName]; if (typeof stackContent === 'string') { stackContent = JSON.parse(stackContent); } await CloudformationProviderFacade.prePushCfnTemplateModifier(context, stackContent); fs.writeFileSync(fullStackPath, JSONUtilities.stringify(stackContent)); } // Write any functions to disk const functionNames = Object.keys(deployment.functions); const functionRootPath = path.normalize(`${directory}/functions`); if (!fs.existsSync(functionRootPath)) { fs.mkdirSync(functionRootPath); } for (const functionName of functionNames) { const fullFunctionPath = path.normalize(`${functionRootPath}/${functionName}`); const zipContents = fs.readFileSync(deployment.functions[functionName]); fs.writeFileSync(fullFunctionPath, zipContents); } const { rootStack } = deployment; const rootStackPath = path.normalize(`${directory}/${rootStackFileName}`); const rootStackString = JSON.stringify(rootStack, null, 4); fs.writeFileSync(rootStackPath, rootStackString); // Write params to disk const jsonString = JSON.stringify(buildParameters, null, 4); const parametersOutputFilePath = path.join(directory, PARAMETERS_FILE_NAME); fs.writeFileSync(parametersOutputFilePath, jsonString); } function initStacksAndResolversDirectories(directory: string) { const resolverRootPath = resolverDirectoryPath(directory); if (!fs.existsSync(resolverRootPath)) { fs.mkdirSync(resolverRootPath); } const stackRootPath = stacksDirectoryPath(directory); if (!fs.existsSync(stackRootPath)) { fs.mkdirSync(stackRootPath); } } function pipelineFunctionDirectoryPath(rootPath: string) { return path.normalize(path.join(rootPath, 'pipelineFunctions')); } function resolverDirectoryPath(rootPath: string) { return path.normalize(`${rootPath}/resolvers`); } function stacksDirectoryPath(rootPath: string) { return path.normalize(`${rootPath}/stacks`); } function throwIfNotJSONExt(stackFile: string) { const extension = path.extname(stackFile); if (extension === '.yaml' || extension === '.yml') { throw new Error(`Yaml is not yet supported. Please convert the CloudFormation stack ${stackFile} to json.`); } if (extension !== '.json') { throw new Error(`Invalid extension ${extension} for stack ${stackFile}`); } } const emptyBuildDirPreserveTsconfig = (directory: string) => { const files = fs.readdirSync(directory); files.forEach((file) => { const fileDir = path.join(directory, file); if (fs.lstatSync(fileDir).isDirectory()) { rimraf.sync(fileDir); } else if (!file.endsWith('tsconfig.resource.json')) { fs.unlinkSync(fileDir); } }); };