From fefa5c689355c78c8b7327e046497affb8402950 Mon Sep 17 00:00:00 2001 From: Ammar <56042290+ammarkarachi@users.noreply.github.com> Date: Tue, 2 Nov 2021 20:34:21 -0700 Subject: [PATCH] feat: amplify export * Refactor/packaging (#8547) * feat(amplify-provider-awscloudformation): refactor of the exporting resources * feat(amplify-provider-awscloudformation): refactored export to write files to external path * refactor: clean up * refactor: remove unused * fix: mispelled url * feat: fall back on push globing and template url * feat: export * feat: modify generation of lambda layer version content * fix(amplify-category-function): lambda layers filter stacks * feat: added command line for export * test(amplify-provider-awscloudformation): added tests for export resources * perf: removed unused * refactor: removed unused * fix: make the tags file pascal case * docs(amplify-provider-awscloudformation): added some documentation * feat(amplify-provider-awscloudformation): added warning and folder perms * fix: check in constants change * refactor: addressing PR feedback * refactor: es6 export * feat: minor changes for integration testing * refactor: pr comments * fix: cleared commented out code removed backup * ci: revert config file to main * Feat: export pull front end (#8488) * feat: export (#8486) * feat(amplify-provider-awscloudformation): refactor of the exporting resources * feat(amplify-provider-awscloudformation): refactored export to write files to external path * refactor: clean up * refactor: remove unused * fix: mispelled url * feat: fall back on push globing and template url * feat: export * feat: modify generation of lambda layer version content * fix(amplify-category-function): lambda layers filter stacks * feat: added command line for export * test(amplify-provider-awscloudformation): added tests for export resources * perf: removed unused * refactor: removed unused * fix: make the tags file pascal case * docs(amplify-provider-awscloudformation): added some documentation * feat(amplify-provider-awscloudformation): added warning and folder perms * fix: check in constants change * refactor: addressing PR feedback * refactor: es6 export * feat: minor changes for integration testing * refactor: pr comments * fix: cleared commented out code removed backup * ci: revert config file to main * feat: wip export pull * feat: amplify export pull to genereate front end config files * fix: merge fixes from export * refactor: removed unused * fix: some language fixes and bug fix with notification * test: e2e tests * test: codecov and test fix * Refactor/packaging (#8547) * feat(amplify-provider-awscloudformation): refactor of the exporting resources * feat(amplify-provider-awscloudformation): refactored export to write files to external path * refactor: clean up * refactor: remove unused * fix: mispelled url * feat: fall back on push globing and template url * feat: export * feat: modify generation of lambda layer version content * fix(amplify-category-function): lambda layers filter stacks * feat: added command line for export * test(amplify-provider-awscloudformation): added tests for export resources * perf: removed unused * refactor: removed unused * fix: make the tags file pascal case * docs(amplify-provider-awscloudformation): added some documentation * feat(amplify-provider-awscloudformation): added warning and folder perms * fix: check in constants change * refactor: addressing PR feedback * refactor: es6 export * feat: minor changes for integration testing * refactor: pr comments * fix: cleared commented out code removed backup * ci: revert config file to main * Feat: export pull front end (#8488) * feat: export (#8486) * feat(amplify-provider-awscloudformation): refactor of the exporting resources * feat(amplify-provider-awscloudformation): refactored export to write files to external path * refactor: clean up * refactor: remove unused * fix: mispelled url * feat: fall back on push globing and template url * feat: export * feat: modify generation of lambda layer version content * fix(amplify-category-function): lambda layers filter stacks * feat: added command line for export * test(amplify-provider-awscloudformation): added tests for export resources * perf: removed unused * refactor: removed unused * fix: make the tags file pascal case * docs(amplify-provider-awscloudformation): added some documentation * feat(amplify-provider-awscloudformation): added warning and folder perms * fix: check in constants change * refactor: addressing PR feedback * refactor: es6 export * feat: minor changes for integration testing * refactor: pr comments * fix: cleared commented out code removed backup * ci: revert config file to main * feat: wip export pull * feat: amplify export pull to genereate front end config files * fix: merge fixes from export * refactor: removed unused * fix: some language fixes and bug fix with notification * test: e2e tests * test: codecov and test fix * feat: export + override reconciled * test: fixed test because of bad merge * fix: added back conditional conditional rebuild * refactor(amplify-provider-awscloudformation): renamed the files to fit * test: fixed idp add storage test * test: fixes type import --- .../types/packaging-types.ts | 1 + .../utils/layerCloudState.ts | 5 +- .../awscloudformation/utils/package.ts | 3 +- .../awscloudformation/utils/packageLayer.ts | 9 +- packages/amplify-cli-core/src/errors/index.ts | 4 + .../src/state-manager/stateManager.ts | 4 +- packages/amplify-cli-core/src/tags/Tags.ts | 12 +- packages/amplify-cli-core/src/utils/index.ts | 1 + .../src/utils/validate-path.ts | 22 + packages/amplify-cli/amplify-plugin.json | 1 + packages/amplify-cli/src/commands/export.ts | 105 ++++ .../amplify-e2e-core/src/categories/auth.ts | 19 +- .../src/categories/storage.ts | 38 +- packages/amplify-e2e-core/src/export/index.ts | 36 ++ packages/amplify-e2e-core/src/index.ts | 2 + .../src/__tests__/export-pull.test.ts | 140 ++++++ packages/amplify-frontend-android/index.js | 27 +- .../lib/frontend-config-creator.js | 56 +-- packages/amplify-frontend-flutter/index.js | 29 +- .../lib/frontend-config-creator.js | 39 +- packages/amplify-frontend-ios/index.js | 28 +- .../lib/frontend-config-creator.js | 33 +- packages/amplify-frontend-javascript/index.js | 18 +- .../lib/frontend-config-creator.js | 18 +- .../src/utils/jwt.ts | 2 +- .../resource-package/resource-export.test.ts | 466 ++++++++++++++++++ .../src/aws-utils/aws-cfn.js | 11 +- .../src/export-resources.ts | 194 ++++++++ .../src/export-types/BuiltResourceType.ts | 10 + .../src/export-types/ResourceType.ts | 6 + .../src/export-update-amplify-meta.ts | 46 ++ .../src/index.ts | 18 +- .../src/push-resources.ts | 31 +- .../src/resource-package/constants.ts | 59 +++ .../src/resource-package/resource-export.ts | 464 +++++++++++++++++ .../src/resource-package/resource-packager.ts | 328 ++++++++++++ .../src/resource-package/types.ts | 60 +++ .../root-stack-transform.ts | 8 +- .../src/utils/env-level-constructs.ts | 42 +- 39 files changed, 2271 insertions(+), 124 deletions(-) create mode 100644 packages/amplify-cli-core/src/utils/validate-path.ts create mode 100644 packages/amplify-cli/src/commands/export.ts create mode 100644 packages/amplify-e2e-core/src/export/index.ts create mode 100644 packages/amplify-e2e-tests/src/__tests__/export-pull.test.ts create mode 100644 packages/amplify-provider-awscloudformation/src/__tests__/resource-package/resource-export.test.ts create mode 100644 packages/amplify-provider-awscloudformation/src/export-resources.ts create mode 100644 packages/amplify-provider-awscloudformation/src/export-types/BuiltResourceType.ts create mode 100644 packages/amplify-provider-awscloudformation/src/export-types/ResourceType.ts create mode 100644 packages/amplify-provider-awscloudformation/src/export-update-amplify-meta.ts create mode 100644 packages/amplify-provider-awscloudformation/src/resource-package/constants.ts create mode 100644 packages/amplify-provider-awscloudformation/src/resource-package/resource-export.ts create mode 100644 packages/amplify-provider-awscloudformation/src/resource-package/resource-packager.ts create mode 100644 packages/amplify-provider-awscloudformation/src/resource-package/types.ts diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/types/packaging-types.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/types/packaging-types.ts index c726aa368f9..1730d3f4681 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/types/packaging-types.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/types/packaging-types.ts @@ -12,4 +12,5 @@ export type PackageRequestMeta = ResourceTuple & { export type Packager = ( context: $TSContext, resource: PackageRequestMeta, + isExport?: boolean, ) => Promise<{ newPackageCreated: boolean; zipFilename: string; zipFilePath: string }>; diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/layerCloudState.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/layerCloudState.ts index c45ad6ca85a..a46eec39898 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/layerCloudState.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/layerCloudState.ts @@ -28,7 +28,10 @@ export class LayerCloudState { const layerVersionList = await lambdaClient.listLayerVersions(isMultiEnvLayer(layerName) ? `${layerName}-${envName}` : layerName); const cfnClient = await providerPlugin.getCloudFormationSdk(context); const stackList = await cfnClient.listStackResources(); - const layerStacks = stackList?.StackResourceSummaries?.filter(stack => stack.LogicalResourceId.includes(layerName)); + const layerStacks = stackList?.StackResourceSummaries?.filter( + // do this because cdk does some rearranging of resources + stack => stack.LogicalResourceId.includes(layerName) && stack.ResourceType === 'AWS::CloudFormation::Stack', + ); let detailedLayerStack; if (layerStacks?.length > 0) { diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/package.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/package.ts index 5a753e0fc19..6a2d95e5b96 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/package.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/package.ts @@ -4,7 +4,8 @@ import { ServiceName } from './constants'; import { packageFunction } from './packageFunction'; import { packageLayer } from './packageLayer'; -export const packageResource: Packager = async (context, resource) => getPackagerForService(resource.service)(context, resource); +export const packageResource: Packager = async (context, resource, isExport) => + getPackagerForService(resource.service)(context, resource, isExport); // there are some other categories (api and maybe others) that depend on the packageFunction function to create a zip file of resource // which is why it is the default return value here diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/packageLayer.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/packageLayer.ts index a26a62bfced..911a6ab27dc 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/packageLayer.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/utils/packageLayer.ts @@ -18,11 +18,11 @@ import { zipPackage } from './zipResource'; /** * Packages lambda layer code and artifacts into a lambda-compatible .zip file */ -export const packageLayer: Packager = async (context, resource) => { +export const packageLayer: Packager = async (context, resource, isExport) => { const previousHash = loadPreviousLayerHash(resource.resourceName); const currentHash = await ensureLayerVersion(context, resource.resourceName, previousHash); - if (previousHash === currentHash) { + if (!isExport && previousHash === currentHash) { // This happens when a Lambda layer's permissions have been updated, but no new layer version needs to be pushed return { newPackageCreated: false, zipFilename: undefined, zipFilePath: undefined }; } @@ -84,7 +84,10 @@ export const packageLayer: Packager = async (context, resource) => { } const zipFilename = createLayerZipFilename(resource.resourceName, layerCloudState.latestVersionLogicalId); - context.amplify.updateAmplifyMetaAfterPackage(resource, zipFilename, { resourceKey: versionHash, hashValue: currentHash }); + if (!isExport) { + // don't apply an update to Amplify meta on export + context.amplify.updateAmplifyMetaAfterPackage(resource, zipFilename, { resourceKey: versionHash, hashValue: currentHash }); + } return { newPackageCreated: true, zipFilename, zipFilePath: destination }; }; diff --git a/packages/amplify-cli-core/src/errors/index.ts b/packages/amplify-cli-core/src/errors/index.ts index aa418fdcd48..843b1ed657d 100644 --- a/packages/amplify-cli-core/src/errors/index.ts +++ b/packages/amplify-cli-core/src/errors/index.ts @@ -17,8 +17,12 @@ export class SchemaDoesNotExistError extends Error {} export class AngularConfigNotFoundError extends Error {} export class AppIdMismatchError extends Error {} export class UnrecognizedFrameworkError extends Error {} +export class UnrecognizedFrontendError extends Error {} export class ConfigurationError extends Error {} export class CustomPoliciesFormatError extends Error {} +export class ExportPathValidationError extends Error {} +export class ExportedStackNotFoundError extends Error {} +export class ExportedStackNotInValidStateError extends Error {} export class NotInitializedError extends Error { public constructor() { diff --git a/packages/amplify-cli-core/src/state-manager/stateManager.ts b/packages/amplify-cli-core/src/state-manager/stateManager.ts index 6e7ba0681d6..cafa3f838ce 100644 --- a/packages/amplify-cli-core/src/state-manager/stateManager.ts +++ b/packages/amplify-cli-core/src/state-manager/stateManager.ts @@ -211,11 +211,11 @@ export class StateManager { JSONUtilities.writeJson(filePath, localAWSInfo); }; - getHydratedTags = (projectPath?: string | undefined): Tag[] => { + getHydratedTags = (projectPath?: string | undefined, skipProjEnv: boolean = false): Tag[] => { const tags = this.getProjectTags(projectPath); const { projectName } = this.getProjectConfig(projectPath); const { envName } = this.getLocalEnvInfo(projectPath); - return HydrateTags(tags, { projectName, envName }); + return HydrateTags(tags, { projectName, envName }, skipProjEnv); }; isTagFilePresent = (projectPath?: string | undefined): boolean => { diff --git a/packages/amplify-cli-core/src/tags/Tags.ts b/packages/amplify-cli-core/src/tags/Tags.ts index 85cd385de4e..38d1c8a4cdc 100644 --- a/packages/amplify-cli-core/src/tags/Tags.ts +++ b/packages/amplify-cli-core/src/tags/Tags.ts @@ -17,7 +17,7 @@ export function ReadTags(tagsFilePath: string): Tag[] { return tags; } -export function validate(tags: Tag[]): void { +export function validate(tags: Tag[], skipProjectEnv: boolean = false): void { const allowedKeySet = new Set(['Key', 'Value']); //check if Tags have the right format @@ -34,7 +34,8 @@ export function validate(tags: Tag[]): void { // check if the tags has valid keys and values _.each(tags, tag => { const tagValidationRegExp = /[^a-z0-9_.:/=+@\- ]/gi; - if (tagValidationRegExp.test(tag.Value)) { + const tagValue = skipProjectEnv ? tag.Value.replace('{project-env}', '') : tag.Value; + if (tagValidationRegExp.test(tagValue)) { throw new Error( 'Invalid character found in Tag Value. Tag values may only contain unicode letters, digits, whitespace, or one of these symbols: _ . : / = + - @', ); @@ -56,19 +57,20 @@ export function validate(tags: Tag[]): void { }); } -export function HydrateTags(tags: Tag[], tagVariables: TagVariables): Tag[] { +export function HydrateTags(tags: Tag[], tagVariables: TagVariables, skipProjectEnv: boolean = false): Tag[] { const { envName, projectName } = tagVariables; const replace: any = { '{project-name}': projectName, '{project-env}': envName, }; + const regexMatcher = skipProjectEnv ? /{project-name}/g : /{project-name}|{project-env}/g; const hydrdatedTags = tags.map(tag => { return { ...tag, - Value: tag.Value.replace(/{project-name}|{project-env}/g, (matched: string) => replace[matched]), + Value: tag.Value.replace(regexMatcher, (matched: string) => replace[matched]), }; }); - validate(hydrdatedTags); + validate(hydrdatedTags, skipProjectEnv); return hydrdatedTags; } diff --git a/packages/amplify-cli-core/src/utils/index.ts b/packages/amplify-cli-core/src/utils/index.ts index 21adb00e95e..80cff7aeb82 100644 --- a/packages/amplify-cli-core/src/utils/index.ts +++ b/packages/amplify-cli-core/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './isResourceNameUnique'; export * from './open'; export * from './packageManager'; export * from './recursiveOmit'; +export * from './validate-path'; diff --git a/packages/amplify-cli-core/src/utils/validate-path.ts b/packages/amplify-cli-core/src/utils/validate-path.ts new file mode 100644 index 00000000000..3008c03050f --- /dev/null +++ b/packages/amplify-cli-core/src/utils/validate-path.ts @@ -0,0 +1,22 @@ +import * as fs from 'fs-extra'; +import { ExportPathValidationError } from '../errors'; + +/** + * Validates whether the path is a directory + * @throws {ExportPathValidationError} if path not valid + * @param directoryPath to validate + */ +export function validateExportDirectoryPath(directoryPath: any) { + if (typeof directoryPath !== 'string') { + throw new ExportPathValidationError(`${directoryPath} is not a valid path specified by --out`); + } + + if (!fs.existsSync(directoryPath)) { + throw new ExportPathValidationError(`${directoryPath} does not exist`); + } + + const stat = fs.lstatSync(directoryPath); + if (!stat.isDirectory()) { + throw new ExportPathValidationError(`${directoryPath} is not a valid directory`); + } +} diff --git a/packages/amplify-cli/amplify-plugin.json b/packages/amplify-cli/amplify-plugin.json index dc90102dda9..1b62a73db38 100644 --- a/packages/amplify-cli/amplify-plugin.json +++ b/packages/amplify-cli/amplify-plugin.json @@ -7,6 +7,7 @@ "console", "delete", "env", + "export", "help", "init", "logout", diff --git a/packages/amplify-cli/src/commands/export.ts b/packages/amplify-cli/src/commands/export.ts new file mode 100644 index 00000000000..5990d7a1ca2 --- /dev/null +++ b/packages/amplify-cli/src/commands/export.ts @@ -0,0 +1,105 @@ +import { $TSContext, IAmplifyResource, stateManager, UnrecognizedFrontendError, validateExportDirectoryPath } from 'amplify-cli-core'; +import { printer } from 'amplify-prompts'; +import chalk from 'chalk'; +import { getResourceOutputs } from '../extensions/amplify-helpers/get-resource-outputs'; +import Ora from 'ora'; +import { getResources } from './build-override'; + +export const run = async (context: $TSContext) => { + const options = context.input.options; + const subCommands = context.input.subCommands; + const showHelp = !options || options.help || !options.out; + const isPull = !!(subCommands && subCommands.includes('pull')); + const showPullHelp = (showHelp || !options.frontend || !options.rootStackName) && isPull; + + if (showHelp && !showPullHelp) { + printer.blankLine(); + printer.info("'amplify export', Allows you to integrate your backend into an external deployment tool"); + printer.blankLine(); + printer.info(`${chalk.yellow('--cdk')} Export all resources with cdk comatibility`); + printer.info(`${chalk.yellow('--out')} Root directory of cdk project`); + printer.blankLine(); + printer.info(`Example: ${chalk.green('amplify export --cdk --out ~/myCDKApp')}`); + printer.blankLine(); + printer.info("'amplify export pull' To export front-end config files'"); + printer.info("'amplify export pull --help' to learn"); + printer.blankLine(); + return; + } + + if (showPullHelp) { + const frontendPlugins = context.amplify.getFrontendPlugins(context); + const frontends = Object.keys(frontendPlugins); + printer.blankLine(); + printer.info("'amplify export pull', Allows you to genreate frontend config files at a desired location"); + printer.blankLine(); + printer.info(`${chalk.yellow('--rooStackName')} Amplify CLI deployed Root Stack name`); + printer.info(`${chalk.yellow('--frontend')} Front end type ex: ${frontends.join(', ')}`); + printer.info(`${chalk.yellow('--out')} Directory to write the front-end config files`); + printer.blankLine(); + printer.info( + `Example: ${chalk.green( + 'amplify export pull --rootStackName amplify-myapp-stack-123 --out ~/myCDKApp/src/config/ --frontend javascript', + )}`, + ); + printer.blankLine(); + printer.blankLine(); + return; + } + const exportPath = context.input.options['out']; + if (isPull) { + await createFrontEndConfigFile(context, exportPath); + } else { + await exportBackend(context, exportPath); + } +}; + +async function exportBackend(context: $TSContext, exportPath: string) { + await buildAllResources(context); + const resources = await context.amplify.getResourceStatus(); + await context.amplify.showResourceTable(); + const providerPlugin = context.amplify.getProviderPlugins(context); + const providers = Object.keys(providerPlugin); + for await (const provider of providers) { + const plugin = await import(providerPlugin[provider]); + await plugin.exportResources(context, resources, exportPath); + } +} + +async function buildAllResources(context: $TSContext) { + const resourcesToBuild: IAmplifyResource[] = await getResources(context); + await context.amplify.executeProviderUtils(context, 'awscloudformation', 'buildOverrides', { resourcesToBuild, forceCompile: true }); +} + +async function createFrontEndConfigFile(context: $TSContext, exportPath: string) { + const { rootStackName, frontend } = context.input.options; + + const frontendSet = new Set(Object.keys(context.amplify.getFrontendPlugins(context))); + if (!frontendSet.has(frontend)) { + throw new UnrecognizedFrontendError(`${frontend} is not a supported Amplify frontend`); + } + const spinner = Ora(`Extracting outputs from ${rootStackName}`); + + spinner.start(); + const providerPlugin = context.amplify.getProviderPlugins(context); + const providers = Object.keys(providerPlugin); + try { + for await (const provider of providers) { + const plugin = await import(providerPlugin[provider]); + await plugin.exportedStackResourcesUpdateMeta(context, rootStackName); + } + spinner.text = `Generating files at ${exportPath}`; + const meta = stateManager.getMeta(); + const cloudMeta = stateManager.getCurrentMeta(); + const frontendPlugins = context.amplify.getFrontendPlugins(context); + const frontendHandlerModule = require(frontendPlugins[frontend]); + validateExportDirectoryPath(exportPath); + await frontendHandlerModule.createFrontendConfigsAtPath(context, getResourceOutputs(meta), getResourceOutputs(cloudMeta), exportPath); + spinner.succeed('Successfully generated frontend config files'); + } catch (ex: any) { + spinner.fail('Failed to generate frontend config files ' + ex.message); + throw ex; + } finally { + spinner.stop(); + } +} diff --git a/packages/amplify-e2e-core/src/categories/auth.ts b/packages/amplify-e2e-core/src/categories/auth.ts index e7c845ebbed..2c35e55002c 100644 --- a/packages/amplify-e2e-core/src/categories/auth.ts +++ b/packages/amplify-e2e-core/src/categories/auth.ts @@ -1038,7 +1038,7 @@ export function addAuthWithMaxOptions(cwd: string, settings: any): Promise } = getSocialProviders(true); return new Promise((resolve, reject) => { - spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true }) + const chain = spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true }) .wait('Do you want to use the default authentication and security configuration?') .send(KEY_DOWN_ARROW) .send(KEY_DOWN_ARROW) @@ -1061,7 +1061,14 @@ export function addAuthWithMaxOptions(cwd: string, settings: any): Promise .sendCarriageReturn() .wait('Enter your Google Web Client ID for your identity pool:') .send('googleIDPOOL') - .sendCarriageReturn() + .sendCarriageReturn(); + if (settings.frontend === 'ios') { + chain.wait('Enter your Google iOS Client ID for your identity pool').send('googleiosclientId').sendCarriageReturn(); + } + if (settings.frontend === 'android') { + chain.wait('Enter your Google Android Client ID for your identity pool').send('googleandroidclientid').sendCarriageReturn(); + } + chain .wait('Enter your Amazon App ID for your identity pool') .send('amazonIDPOOL') .sendCarriageReturn() @@ -1137,9 +1144,11 @@ export function addAuthWithMaxOptions(cwd: string, settings: any): Promise .wait('Enter your redirect signout URI') .sendLine('https://signout1/') .wait('Do you want to add another redirect signout URI') - .sendConfirmNo() - .wait('Select the OAuth flows enabled for this project') - .sendCarriageReturn() + .sendConfirmNo(); + if (settings.frontend !== 'ios' && settings.frontend !== 'android' && settings.frontend !== 'flutter') { + chain.wait('Select the OAuth flows enabled for this project').sendCarriageReturn(); + } + chain .wait('Select the OAuth scopes enabled for this project') .sendCarriageReturn() .wait('Select the social providers you want to configure for your user pool') diff --git a/packages/amplify-e2e-core/src/categories/storage.ts b/packages/amplify-e2e-core/src/categories/storage.ts index 8230647ec98..c49987bd373 100644 --- a/packages/amplify-e2e-core/src/categories/storage.ts +++ b/packages/amplify-e2e-core/src/categories/storage.ts @@ -537,6 +537,43 @@ export function updateS3AddTrigger(cwd: string, settings: any): Promise { }); } +export function addS3StorageWithIdpAuth(projectDir: string): Promise { + return new Promise((resolve, reject) => { + let chain = spawn(getCLIPath(), ['add', 'storage'], { cwd: projectDir, stripColors: true }); + + singleSelect(chain.wait('Please select from one of the below mentioned services:'), 'Content (Images, audio, video, etc.)', [ + 'Content (Images, audio, video, etc.)', + 'NoSQL Database', + ]); + + chain + .wait('Provide a friendly name for your resource that will be used to label this category in the project:') + .sendCarriageReturn() + .wait('Provide bucket name:') + .sendCarriageReturn() + .wait('Restrict access by') + .sendCarriageReturn() + .wait('Who should have access:') + .sendCarriageReturn(); + + multiSelect( + chain.wait('What kind of access do you want for Authenticated users?'), + ['create/update', 'read', 'delete'], + ['create/update', 'read', 'delete'], + ); + + chain.wait('Do you want to add a Lambda Trigger for your S3 Bucket?').sendConfirmNo(); + + chain.run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + export function addS3Storage(projectDir: string): Promise { return new Promise((resolve, reject) => { let chain = spawn(getCLIPath(), ['add', 'storage'], { cwd: projectDir, stripColors: true }); @@ -595,7 +632,6 @@ export function overrideS3(cwd: string, settings: {}) { }); } - export function addS3StorageWithSettings(projectDir: string, settings: AddStorageSettings): Promise { return new Promise((resolve, reject) => { let chain = spawn(getCLIPath(), ['add', 'storage'], { cwd: projectDir, stripColors: true }); diff --git a/packages/amplify-e2e-core/src/export/index.ts b/packages/amplify-e2e-core/src/export/index.ts new file mode 100644 index 00000000000..8799e8ad2a8 --- /dev/null +++ b/packages/amplify-e2e-core/src/export/index.ts @@ -0,0 +1,36 @@ +import { nspawn as spawn, getCLIPath } from '..'; + +export function exportBackend(cwd: string, settings: { exportPath: string }): Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['export', '--out', settings.exportPath], { cwd, stripColors: true }) + .wait('For more information: docs.amplify.aws/cli/export') + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function exportPullBackend(cwd: string, settings: { exportPath: string; frontend: string; rootStackName: string }): Promise { + return new Promise((resolve, reject) => { + spawn( + getCLIPath(), + ['export', 'pull', '--out', settings.exportPath, '--frontend', settings.frontend, '--rootStackName', settings.rootStackName], + { cwd, stripColors: true }, + ) + .wait('Successfully generated frontend config files') + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + diff --git a/packages/amplify-e2e-core/src/index.ts b/packages/amplify-e2e-core/src/index.ts index 048d2576415..4a71d359668 100644 --- a/packages/amplify-e2e-core/src/index.ts +++ b/packages/amplify-e2e-core/src/index.ts @@ -12,6 +12,7 @@ export * from './init/'; export * from './utils/'; export * from './categories'; export * from './utils/sdk-calls'; +export * from './export/'; export { addFeatureFlag } from './utils/feature-flags'; declare global { @@ -43,6 +44,7 @@ export function isTestingWithLatestCodebase(scriptRunnerPath) { export function getScriptRunnerPath(testingWithLatestCodebase = false) { if (!testingWithLatestCodebase) { + return process.platform === 'win32' ? 'node.exe' : 'exec'; } diff --git a/packages/amplify-e2e-tests/src/__tests__/export-pull.test.ts b/packages/amplify-e2e-tests/src/__tests__/export-pull.test.ts new file mode 100644 index 00000000000..c749b7f4bcd --- /dev/null +++ b/packages/amplify-e2e-tests/src/__tests__/export-pull.test.ts @@ -0,0 +1,140 @@ +import { + addApiWithoutSchema, + addAuthWithMaxOptions, + addConvert, + addDEVHosting, + addFunction, + addInterpret, + addRestApi, + addS3Storage, + addS3StorageWithIdpAuth, + addSampleInteraction, + addSMSNotification, + amplifyPush, + amplifyPushAuth, + amplifyPushWithoutCodegen, + amplifyPushWithUpdate, + createNewProjectDir, + deleteProject, + deleteProjectDir, + exportPullBackend, + getAmplifyConfigAndroidPath, + getAmplifyConfigIOSPath, + getAmplifyIOSConfig, + getAWSConfigAndroidPath, + getAWSConfigIOSPath, + getBackendAmplifyMeta, + initAndroidProjectWithProfile, + initFlutterProjectWithProfile, + initIosProjectWithProfile, + initJSProjectWithProfile, +} from 'amplify-e2e-core'; +import { getAWSExportsPath } from '../aws-exports/awsExports'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as _ from 'lodash'; + +describe('amplify export pull', () => { + let projRoot: string; + beforeEach(async () => { + projRoot = await createNewProjectDir('exporttest'); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('init a js project and compare with export pull', async () => { + await initJSProjectWithProfile(projRoot, { envName: 'dev' }); + await AddandPushCategories(); + const exportsPath = getAWSExportsPath(projRoot); + const pathToExportGeneratedConfig = await generatePullConfig('javascript'); + compareFileContents(exportsPath, path.join(pathToExportGeneratedConfig, path.basename(exportsPath))); + }); + + it('init an ios project and compare with export pull', async () => { + await initIosProjectWithProfile(projRoot, { envName: 'dev' }); + + await AddandPushCategories('ios'); + const awsConfigPath = getAWSConfigIOSPath(projRoot); + const amplifyConfigPath = getAmplifyConfigIOSPath(projRoot); + const pullConfigPath = await generatePullConfig('ios'); + compareFileContents(awsConfigPath, path.join(pullConfigPath, path.basename(awsConfigPath))); + compareFileContents(amplifyConfigPath, path.join(pullConfigPath, path.basename(amplifyConfigPath))); + }); + + it('init an android project and compare with export pull', async () => { + await initAndroidProjectWithProfile(projRoot, { envName: 'dev' }); + + await AddandPushCategories('android'); + const awsConfigPath = getAWSConfigAndroidPath(projRoot); + const amplifyConfigPath = getAmplifyConfigAndroidPath(projRoot); + const pullConfigPath = await generatePullConfig('android'); + compareFileContents(awsConfigPath, path.join(pullConfigPath, path.basename(awsConfigPath))); + compareFileContents(amplifyConfigPath, path.join(pullConfigPath, path.basename(amplifyConfigPath))); + }); + + it('init a flutter project and compare with export pull', async () => { + await initFlutterProjectWithProfile(projRoot, { envName: 'dev' }); + await AddandPushCategories('flutter'); + const amplifyConfigPath = path.join(projRoot, 'lib', 'amplifyconfiguration.dart'); + const pullConfigPath = await generatePullConfig('flutter'); + compareFileContents(amplifyConfigPath, path.join(pullConfigPath, path.basename(amplifyConfigPath))); + }); + + function compareFileContents(path1: string, path2: string) { + const fileString1 = fs.readFileSync(path1, 'utf-8'); + const fileString2 = fs.readFileSync(path2, 'utf-8'); + const object1 = JSON.parse(fileString1.substring(fileString1.indexOf('{'), fileString1.lastIndexOf('}') + 1)); + const object2 = JSON.parse(fileString2.substring(fileString2.indexOf('{'), fileString2.lastIndexOf('}') + 1)); + expect(recursiveComapre(object1, object2)).toBeTruthy(); + } + + function recursiveComapre(object1: Object, object2: Object): boolean { + return Object.keys(object1).reduce((equal, key) => { + if (!equal) return false; + if (typeof object1[key] !== 'object') { + return object1[key] === object2[key]; + } else { + return recursiveComapre(object1[key], object2[key]); + } + }, true); + } + + async function AddandPushCategories(frontend?: string) { + await addAuthWithMaxOptions(projRoot, { frontend }); + await addSampleInteraction(projRoot, {}); + await addFunction(projRoot, { functionTemplate: 'Hello World' }, 'nodejs'); + await addApiWithoutSchema(projRoot); + + await addSMSNotification(projRoot, { resourceName: 'export-test' }); + await addRestApi(projRoot, { + existingLambda: false, + isCrud: false, + isFirstRestApi: false, + }); + await addDEVHosting(projRoot); + await addS3StorageWithIdpAuth(projRoot); + await addConvert(projRoot, {}); + await addInterpret(projRoot, {}); + if (frontend === 'flutter') { + await amplifyPushWithoutCodegen(projRoot); + } else { + await amplifyPush(projRoot); + } + } + + async function generatePullConfig(frontend: string) { + const meta = getBackendAmplifyMeta(projRoot); + const stackName = _.get(meta, ['providers', 'awscloudformation', 'StackName']); + const pathToExportGeneratedConfig = path.join(projRoot, 'exportSrc'); + fs.ensureDir(pathToExportGeneratedConfig); + await exportPullBackend(projRoot, { + exportPath: pathToExportGeneratedConfig, + frontend, + rootStackName: stackName, + }); + return pathToExportGeneratedConfig; + } +}); diff --git a/packages/amplify-frontend-android/index.js b/packages/amplify-frontend-android/index.js index 6d908f5c87b..4eac2516ef9 100644 --- a/packages/amplify-frontend-android/index.js +++ b/packages/amplify-frontend-android/index.js @@ -4,7 +4,14 @@ const initializer = require('./lib/initializer'); const configManager = require('./lib/configuration-manager'); const projectScanner = require('./lib/project-scanner'); const constants = require('./lib/constants'); -const { createAmplifyConfig, createAWSConfig, deleteAmplifyConfig } = require('./lib/frontend-config-creator'); +const { + createAmplifyConfig, + getAmplifyConfig, + createAWSConfig, + getNewAWSConfigObject, + deleteAmplifyConfig, + writeToFile, +} = require('./lib/frontend-config-creator'); const pluginName = 'android'; @@ -19,6 +26,23 @@ function init(context) { function onInitSuccessful(context) { return initializer.onInitSuccessful(context); } +/** + * This function enables export to write these files to an external path + * @param {TSContext} context + * @param {metaWithOutput} amplifyResources + * @param {cloudMetaWithOuput} amplifyCloudResources + * @param {string} exportPath path to where the files need to be written + */ +function createFrontendConfigsAtPath(context, amplifyResources, amplifyCloudResources, exportPath) { + const newOutputsForFrontend = amplifyResources.outputsForFrontend; + const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; + + const amplifyConfig = getAmplifyConfig(context, newOutputsForFrontend, cloudOutputsForFrontend); + writeToFile(exportPath, constants.amplifyConfigFilename, amplifyConfig); + + const awsConfig = getNewAWSConfigObject(context, newOutputsForFrontend, cloudOutputsForFrontend); + writeToFile(exportPath, constants.awsConfigFilename, awsConfig); +} function createFrontendConfigs(context, amplifyResources, amplifyCloudResources) { const newOutputsForFrontend = amplifyResources.outputsForFrontend; @@ -73,6 +97,7 @@ module.exports = { publish, run, createFrontendConfigs, + createFrontendConfigsAtPath, displayFrontendDefaults, setFrontendDefaults, executeAmplifyCommand, diff --git a/packages/amplify-frontend-android/lib/frontend-config-creator.js b/packages/amplify-frontend-android/lib/frontend-config-creator.js index f7103cfcb78..4eb46cc9c3e 100644 --- a/packages/amplify-frontend-android/lib/frontend-config-creator.js +++ b/packages/amplify-frontend-android/lib/frontend-config-creator.js @@ -55,22 +55,25 @@ function getSrcDir(context) { } function createAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { - const { amplify } = context; - const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; - const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; - const frontendConfig = projectConfig.config; - const srcDirPath = path.join(projectPath, frontendConfig.ResDir, 'raw'); + const srcDirPath = getSrcDir(context).srcDirPath; - fs.ensureDirSync(srcDirPath); + // Native GA release requires entire awsconfiguration inside amplifyconfiguration auth plugin + const amplifyConfig = getAmplifyConfig(context, amplifyResources, cloudAmplifyResources); - const targetFilePath = path.join(srcDirPath, constants.amplifyConfigFilename); + writeToFile(srcDirPath, constants.amplifyConfigFilename, amplifyConfig); +} - // Native GA release requires entire awsconfiguration inside amplifyconfiguration auth plugin +function writeToFile(filePath, fileName, configObject) { + fs.ensureDirSync(filePath); + const targetFilePath = path.join(filePath, fileName); + const jsonString = JSON.stringify(configObject, null, 4); + fs.writeFileSync(targetFilePath, jsonString, 'utf8'); +} + +function getAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { const newAWSConfig = getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources); const amplifyConfig = amplifyConfigHelper.generateConfig(context, newAWSConfig); - - const jsonString = JSON.stringify(amplifyConfig, null, 4); - fs.writeFileSync(targetFilePath, jsonString, 'utf8'); + return amplifyConfig; } function getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources) { @@ -137,15 +140,19 @@ function getCurrentAWSConfig(context) { const { amplify } = context; const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; - const frontendConfig = projectConfig.config; - const srcDirPath = path.join(projectPath, frontendConfig.ResDir, 'raw'); - - const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); let awsConfig = {}; - if (fs.existsSync(targetFilePath)) { - awsConfig = amplify.readJsonFile(targetFilePath); + if (projectConfig) { + const frontendConfig = projectConfig.config; + const srcDirPath = path.join(projectPath, frontendConfig.ResDir, 'raw'); + + const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); + + if (fs.existsSync(targetFilePath)) { + awsConfig = amplify.readJsonFile(targetFilePath); + } } + return awsConfig; } @@ -160,17 +167,8 @@ function getCustomConfigs(cloudAWSConfig, currentAWSConfig) { } function generateAWSConfigFile(context, configOutput) { - const { amplify } = context; - const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; - const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; - const frontendConfig = projectConfig.config; - const srcDirPath = path.join(projectPath, frontendConfig.ResDir, 'raw'); - - fs.ensureDirSync(srcDirPath); - - const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); - const jsonString = JSON.stringify(configOutput, null, 4); - fs.writeFileSync(targetFilePath, jsonString, 'utf8'); + const { srcDirPath } = getSrcDir(context); + writeToFile(srcDirPath, constants.awsConfigFilename, configOutput); } function getCognitoConfig(cognitoResources, projectRegion) { @@ -391,4 +389,4 @@ function getSumerianConfig(sumerianResources) { }; } -module.exports = { createAWSConfig, createAmplifyConfig, deleteAmplifyConfig }; +module.exports = { createAWSConfig, getNewAWSConfigObject, createAmplifyConfig, getAmplifyConfig, deleteAmplifyConfig, writeToFile }; diff --git a/packages/amplify-frontend-flutter/index.js b/packages/amplify-frontend-flutter/index.js index 271ae8cffbf..2a05da71ece 100644 --- a/packages/amplify-frontend-flutter/index.js +++ b/packages/amplify-frontend-flutter/index.js @@ -5,7 +5,13 @@ const initializer = require('./lib/initializer'); const configManager = require('./lib/configuration-manager'); const projectScanner = require('./lib/project-scanner'); const constants = require('./lib/constants'); -const { createAmplifyConfig, createAWSConfig, deleteAmplifyConfig } = require('./lib/frontend-config-creator'); +const { + createAmplifyConfig, + createAWSConfig, + deleteAmplifyConfig, + getAmplifyConfig, + writeToFile, +} = require('./lib/frontend-config-creator'); const pluginName = 'flutter'; @@ -23,6 +29,26 @@ function onInitSuccessful(context) { return initializer.onInitSuccessful(context); } +/** + * This function enables export to write these files to an external path + * @param {TSContext} context + * @param {metaWithOutput} amplifyResources + * @param {cloudMetaWithOuput} amplifyCloudResources + * @param {string} exportPath path to where the files need to be written + */ +function createFrontendConfigsAtPath(context, amplifyResources, amplifyCloudResources, exportPath) { + const newOutputsForFrontend = amplifyResources.outputsForFrontend; + const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; + + const amplifyConfig = getAmplifyConfig( + context, + newOutputsForFrontend, + cloudOutputsForFrontend, + path.join(exportPath, constants.amplifyConfigFilename), + ); + writeToFile(exportPath, constants.amplifyConfigFilename, amplifyConfig); +} + function createFrontendConfigs(context, amplifyResources, amplifyCloudResources) { const newOutputsForFrontend = amplifyResources.outputsForFrontend; const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; @@ -85,6 +111,7 @@ module.exports = { displayFrontendDefaults, setFrontendDefaults, createFrontendConfigs, + createFrontendConfigsAtPath, executeAmplifyCommand, handleAmplifyEvent, deleteConfig: deleteAmplifyConfig, diff --git a/packages/amplify-frontend-flutter/lib/frontend-config-creator.js b/packages/amplify-frontend-flutter/lib/frontend-config-creator.js index aea9c7eb175..9fc65b08f29 100644 --- a/packages/amplify-frontend-flutter/lib/frontend-config-creator.js +++ b/packages/amplify-frontend-flutter/lib/frontend-config-creator.js @@ -45,15 +45,15 @@ function getSrcDir(context) { } function createAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { - const { amplify } = context; - const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; - const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; - const frontendConfig = projectConfig.config; - const srcDirPath = path.join(projectPath, frontendConfig.ResDir); - - fs.ensureDirSync(srcDirPath); + const srcDirPath = getSrcDir(context).srcDirPath; const targetFilePath = path.join(srcDirPath, constants.amplifyConfigFilename); + let amplifyConfig = getAmplifyConfig(context, amplifyResources, cloudAmplifyResources, targetFilePath); + + writeToFile(srcDirPath, constants.amplifyConfigFilename, amplifyConfig); +} + +function getAmplifyConfig(context, amplifyResources, cloudAmplifyResources, targetFilePath) { let amplifyConfig; if (fs.existsSync(targetFilePath)) { amplifyConfig = readJsonFromDart(targetFilePath); @@ -62,9 +62,13 @@ function createAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { // Native GA release requires entire awsconfiguration inside amplifyconfiguration auth plugin const newAWSConfig = getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources); amplifyConfig = amplifyConfigHelper.generateConfig(context, amplifyConfig, newAWSConfig); + return amplifyConfig; +} - const jsonString = JSON.stringify(amplifyConfig, null, 4); - // fs.writeFileSync(targetFilePath, jsonString, 'utf8'); +function writeToFile(filePath, fileName, configObject) { + fs.ensureDirSync(filePath); + const targetFilePath = path.join(filePath, fileName); + const jsonString = JSON.stringify(configObject, null, 4); writeJsonToDart(targetFilePath, jsonString, null); } @@ -131,14 +135,17 @@ function getCurrentAWSConfig(context) { const { amplify } = context; const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; - const frontendConfig = projectConfig.config; - const srcDirPath = path.join(projectPath, frontendConfig.ResDir); - - const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); let awsConfig = {}; - if (fs.existsSync(targetFilePath)) { - awsConfig = amplify.readJsonFile(targetFilePath); + if (projectConfig) { + const frontendConfig = projectConfig.config; + const srcDirPath = path.join(projectPath, frontendConfig.ResDir); + + const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); + + if (fs.existsSync(targetFilePath)) { + awsConfig = amplify.readJsonFile(targetFilePath); + } } return awsConfig; } @@ -368,4 +375,4 @@ function getSumerianConfig(sumerianResources) { }; } -module.exports = { createAWSConfig, createAmplifyConfig, deleteAmplifyConfig }; +module.exports = { createAWSConfig, createAmplifyConfig, getAmplifyConfig, deleteAmplifyConfig, writeToFile }; diff --git a/packages/amplify-frontend-ios/index.js b/packages/amplify-frontend-ios/index.js index 4acfcbc162e..201320e43c8 100644 --- a/packages/amplify-frontend-ios/index.js +++ b/packages/amplify-frontend-ios/index.js @@ -6,7 +6,14 @@ const initializer = require('./lib/initializer'); const projectScanner = require('./lib/project-scanner'); const configManager = require('./lib/configuration-manager'); const constants = require('./lib/constants'); -const { createAmplifyConfig, createAWSConfig, deleteAmplifyConfig } = require('./lib/frontend-config-creator'); +const { + createAmplifyConfig, + getNewAWSConfigObject, + createAWSConfig, + deleteAmplifyConfig, + getAmplifyConfig, + writeToFile, +} = require('./lib/frontend-config-creator'); const pluginName = 'ios'; @@ -30,6 +37,24 @@ function setFrontendDefaults(context, projectPath) { return configManager.setFrontendDefaults(context); } +/** + * This function enables export to write these files to an external path + * @param {TSContext} context + * @param {metaWithOutput} amplifyResources + * @param {cloudMetaWithOuput} amplifyCloudResources + * @param {string} exportPath path to where the files need to be written + */ +function createFrontendConfigsAtPath(context, amplifyResources, amplifyCloudResources, exportPath) { + const newOutputsForFrontend = amplifyResources.outputsForFrontend; + const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; + + const amplifyConfig = getAmplifyConfig(context, newOutputsForFrontend, cloudOutputsForFrontend); + writeToFile(exportPath, constants.amplifyConfigFilename, amplifyConfig); + + const awsConfig = getNewAWSConfigObject(context, newOutputsForFrontend, cloudOutputsForFrontend); + writeToFile(exportPath, constants.awsConfigFilename, awsConfig); +} + function createFrontendConfigs(context, amplifyResources, amplifyCloudResources) { const newOutputsForFrontend = amplifyResources.outputsForFrontend; const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; @@ -122,6 +147,7 @@ module.exports = { publish, run, createFrontendConfigs, + createFrontendConfigsAtPath, executeAmplifyCommand, handleAmplifyEvent, deleteConfig: deleteAmplifyConfig, diff --git a/packages/amplify-frontend-ios/lib/frontend-config-creator.js b/packages/amplify-frontend-ios/lib/frontend-config-creator.js index 1969134f8fa..828fbd07ffb 100644 --- a/packages/amplify-frontend-ios/lib/frontend-config-creator.js +++ b/packages/amplify-frontend-ios/lib/frontend-config-creator.js @@ -39,22 +39,34 @@ function getSrcDir(context) { } function createAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { - const { amplify } = context; - const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; - const srcDirPath = path.join(projectPath); + const srcDirPath = getSrcDir(context); if (fs.existsSync(srcDirPath)) { const targetFilePath = path.join(srcDirPath, constants.amplifyConfigFilename); // Native GA release requires entire awsconfiguration inside amplifyconfiguration auth plugin - const newAWSConfig = getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources); - const amplifyConfig = amplifyConfigHelper.generateConfig(context, newAWSConfig); + const amplifyConfig = getAmplifyConfig(context, amplifyResources, cloudAmplifyResources); const jsonString = JSON.stringify(amplifyConfig, null, 4); fs.writeFileSync(targetFilePath, jsonString, 'utf8'); + + writeToFile(srcDirPath, constants.amplifyConfigFilename, amplifyConfig); } } +function writeToFile(filePath, fileName, configObject) { + fs.ensureDirSync(filePath); + const targetFilePath = path.join(filePath, fileName); + const jsonString = JSON.stringify(configObject, null, 4); + fs.writeFileSync(targetFilePath, jsonString, 'utf8'); +} + +function getAmplifyConfig(context, amplifyResources, cloudAmplifyResources) { + const newAWSConfig = getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources); + const amplifyConfig = amplifyConfigHelper.generateConfig(context, newAWSConfig); + return amplifyConfig; +} + function getNewAWSConfigObject(context, amplifyResources, cloudAmplifyResources) { const newAWSConfig = getAWSConfigObject(amplifyResources); const cloudAWSConfig = getAWSConfigObject(cloudAmplifyResources); @@ -139,14 +151,9 @@ function getCustomConfigs(cloudAWSConfig, currentAWSConfig) { } function generateAWSConfigFile(context, configOutput) { - const { amplify } = context; - const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; - const srcDirPath = path.join(projectPath); - + const srcDirPath = getSrcDir(context); if (fs.existsSync(srcDirPath)) { - const targetFilePath = path.join(srcDirPath, constants.awsConfigFilename); - const jsonString = JSON.stringify(configOutput, null, 4); - fs.writeFileSync(targetFilePath, jsonString, 'utf8'); + writeToFile(srcDirPath, constants.awsConfigFilename, configOutput); } } @@ -373,4 +380,4 @@ function getSumerianConfig(sumerianResources) { }; } -module.exports = { createAWSConfig, createAmplifyConfig, deleteAmplifyConfig }; +module.exports = { createAWSConfig, getNewAWSConfigObject, createAmplifyConfig, getAmplifyConfig, deleteAmplifyConfig, writeToFile }; diff --git a/packages/amplify-frontend-javascript/index.js b/packages/amplify-frontend-javascript/index.js index b7d89a39825..9a387fb37aa 100644 --- a/packages/amplify-frontend-javascript/index.js +++ b/packages/amplify-frontend-javascript/index.js @@ -7,7 +7,7 @@ const configManager = require('./lib/configuration-manager'); const server = require('./lib/server'); const publisher = require('./lib/publisher'); const constants = require('./lib/constants'); -const { createAWSExports, deleteAmplifyConfig } = require('./lib/frontend-config-creator'); +const { createAWSExports, getAWSExports, deleteAmplifyConfig, generateAwsExportsAtPath } = require('./lib/frontend-config-creator'); const pluginName = 'javascript'; @@ -25,6 +25,21 @@ function onInitSuccessful(context) { return initializer.onInitSuccessful(context); } +/** + * This function enables export to write these files to an external path + * @param {TSContext} context + * @param {metaWithOutput} amplifyResources + * @param {cloudMetaWithOuput} amplifyCloudResources + * @param {string} exportPath path to where the files need to be written + */ +async function createFrontendConfigsAtPath(context, amplifyResources, amplifyCloudResources, exportPath) { + const newOutputsForFrontend = amplifyResources.outputsForFrontend; + const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; + + const amplifyConfig = await getAWSExports(context, newOutputsForFrontend, cloudOutputsForFrontend); + generateAwsExportsAtPath(context, path.join(exportPath, constants.exportsFilename), amplifyConfig); +} + async function createFrontendConfigs(context, amplifyResources, amplifyCloudResources) { const newOutputsForFrontend = amplifyResources.outputsForFrontend; const cloudOutputsForFrontend = amplifyCloudResources.outputsForFrontend; @@ -87,6 +102,7 @@ module.exports = { publish, run, createFrontendConfigs, + createFrontendConfigsAtPath, initializeAwsExports, executeAmplifyCommand, handleAmplifyEvent, diff --git a/packages/amplify-frontend-javascript/lib/frontend-config-creator.js b/packages/amplify-frontend-javascript/lib/frontend-config-creator.js index dd14e9da741..45ed419e614 100644 --- a/packages/amplify-frontend-javascript/lib/frontend-config-creator.js +++ b/packages/amplify-frontend-javascript/lib/frontend-config-creator.js @@ -76,6 +76,12 @@ function createAmplifyConfig(context, amplifyResources) { } async function createAWSExports(context, amplifyResources, cloudAmplifyResources) { + const newAWSExports = await getAWSExports(context, amplifyResources, cloudAmplifyResources); + generateAWSExportsFile(context, newAWSExports); + return context; +} + +async function getAWSExports(context, amplifyResources, cloudAmplifyResources) { const newAWSExports = getAWSExportsObject(amplifyResources); const cloudAWSExports = getAWSExportsObject(cloudAmplifyResources); const currentAWSExports = await getCurrentAWSExports(context); @@ -83,8 +89,7 @@ async function createAWSExports(context, amplifyResources, cloudAmplifyResources const customConfigs = getCustomConfigs(cloudAWSExports, currentAWSExports); Object.assign(newAWSExports, customConfigs); - generateAWSExportsFile(context, newAWSExports); - return context; + return newAWSExports; } function getCustomConfigs(cloudAWSExports, currentAWSExports) { @@ -227,7 +232,6 @@ async function getCurrentAWSExports(context) { async function generateAWSExportsFile(context, configOutput) { const { amplify } = context; - const pluginDir = __dirname; const projectPath = context.exeInfo ? context.exeInfo.localEnvInfo.projectPath : amplify.getEnvInfo().projectPath; const projectConfig = context.exeInfo ? context.exeInfo.projectConfig[constants.Label] : amplify.getProjectConfig()[constants.Label]; const frontendConfig = projectConfig.config; @@ -236,6 +240,12 @@ async function generateAWSExportsFile(context, configOutput) { fs.ensureDirSync(srcDirPath); const targetFilePath = path.join(srcDirPath, constants.exportsFilename); + await generateAwsExportsAtPath(context, targetFilePath, configOutput); +} + +async function generateAwsExportsAtPath(context, targetFilePath, configOutput) { + const pluginDir = __dirname; + const { amplify } = context; const options = { configOutput, }; @@ -593,4 +603,4 @@ function getPlaceIndexConfig(placeIndexResources) { return placeIndexConfig; } -module.exports = { createAWSExports, createAmplifyConfig, deleteAmplifyConfig, getAWSExportsObject }; +module.exports = { createAWSExports, getAWSExports, createAmplifyConfig, deleteAmplifyConfig, generateAwsExportsAtPath }; diff --git a/packages/amplify-graphiql-explorer/src/utils/jwt.ts b/packages/amplify-graphiql-explorer/src/utils/jwt.ts index eaa51bb0d6a..4a9184090eb 100644 --- a/packages/amplify-graphiql-explorer/src/utils/jwt.ts +++ b/packages/amplify-graphiql-explorer/src/utils/jwt.ts @@ -16,7 +16,7 @@ export function generateToken(decodedToken: string | object): string { export function parse(token): object { const decodedToken = decode(token); - return decodedToken; + return decodedToken as object; } /** diff --git a/packages/amplify-provider-awscloudformation/src/__tests__/resource-package/resource-export.test.ts b/packages/amplify-provider-awscloudformation/src/__tests__/resource-package/resource-export.test.ts new file mode 100644 index 00000000000..e097a6cd3ce --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/__tests__/resource-package/resource-export.test.ts @@ -0,0 +1,466 @@ +import { + $TSContext, + CFNTemplateFormat, + JSONUtilities, + pathManager, + readCFNTemplate, + buildOverrideDir, + stateManager, +} from 'amplify-cli-core'; +import { DeploymentResources, PackagedResourceDefinition, ResourceDeployType, StackParameters } from '../../resource-package/types'; +import * as fs from 'fs-extra'; + +const mockMeta = jest.fn(() => { + return { + providers: { + awscloudformation: { + AuthRoleName: 'amplify-amplifyexportest-dev-172019-authRole', + Region: 'us-east-2', + DeploymentBucketName: 'amplify-amplifyexportest-dev-172019-deployment', + UnauthRoleName: 'amplify-amplifyexportest-dev-172019-unauthRole', + StackName: 'amplify-amplifyexportest-dev-172019', + }, + }, + }; +}); + +jest.mock('amplify-cli-core'); +const stateManager_mock = stateManager as jest.Mocked; +stateManager_mock.getMeta = mockMeta; +stateManager_mock.getTeamProviderInfo = jest.fn().mockReturnValue({}); +stateManager_mock.getLocalEnvInfo = jest.fn().mockReturnValue({ envName: 'dev' }); + +const pathManager_mock = pathManager as jest.Mocked; +pathManager_mock.findProjectRoot = jest.fn().mockReturnValue('projectpath'); +pathManager_mock.getBackendDirPath = jest.fn().mockReturnValue('backend'); + +const JSONUtilities_mock = JSONUtilities as jest.Mocked; +JSONUtilities_mock.readJson.mockImplementation((pathToJson: string) => { + if (pathToJson.includes('function') && pathToJson.includes('amplifyexportestlayer5f16d693')) { + return lambdaTemplate; + } + if (pathToJson.includes('rootStackTemplate.json')) { + return { + Resources: { + DeploymentBucket: { + Properties: {}, + }, + }, + Parameters: {}, + } as unknown as Template; + } +}); +const readCFNTemplate_mock = readCFNTemplate as jest.MockedFunction; +readCFNTemplate_mock.mockImplementation(path => { + if (path.includes('function') && path.includes('amplifyexportestlayer5f16d693')) { + return { + cfnTemplate: lambdaTemplate, + templateFormat: CFNTemplateFormat.JSON, + }; + } + return { + cfnTemplate: { + Parameters: {}, + Resources: { + LambdaFunction: { + Properties: {}, + }, + }, + } as unknown as Template, + templateFormat: CFNTemplateFormat.JSON, + }; +}); +const buildOverrideDir_mock = buildOverrideDir as jest.MockedFunction; + +buildOverrideDir_mock.mockImplementation(async (cwd: string, dest: string) => false); + +jest.mock('fs-extra'); +const fs_mock = fs as jest.Mocked; +fs_mock.existsSync.mockReturnValue(true); +fs_mock.lstatSync.mockImplementation((_path, _options) => { + return { + isDirectory: jest.fn().mockReturnValue(true), + } as unknown as fs.Stats; +}); + +jest.mock('../../aws-utils/aws-s3', () => ({ + S3: { + getInstance: jest.fn().mockReturnValue(mockS3Instance), + }, +})); +jest.mock('../../aws-utils/aws-cfn', () => ({ + Cloudformation: {}, +})); +jest.mock('../../admin-modelgen', () => ({ + adminModelgen: {}, +})); +jest.mock('../../display-helpful-urls', () => ({ + displayHelpfulURLs: jest.fn(), +})); +jest.mock('../../zip-util', () => ({ + downloadZip: mockdownloadZip, +})); + +jest.mock('../../download-api-models', () => ({})); +jest.mock('../../graphql-transformer', () => ({})); +jest.mock('../../amplify-service-manager', () => ({})); +jest.mock('../../iterative-deployment', () => ({})); +jest.mock('../../utils/env-level-constructs', () => ({ + getNetworkResourceCfn: jest.fn().mockReturnValue({ Resources: { mocknetworkstack: {} } } as unknown as Template), +})); +jest.mock('../../utils/consolidate-apigw-policies', () => ({ + consolidateApiGatewayPolicies: mockconsolidateApiGatewayPolicies, + loadApiWithPrivacyParams: jest.fn(), +})); +jest.mock('../../transform-graphql-schema', () => ({ + transformGraphQLSchema: mockTransformGql, +})); + +const mockconsolidateApiGatewayPolicies = jest.fn(() => { + return { + APIGatewayAuthURL: 'mockURL', + }; +}); +const mockdownloadZip = jest.fn(); +const mockTransformGql = jest.fn(); +const mockS3Instance = jest.fn(); + +const mockResource: DeploymentResources = { + resourcesToBeCreated: [ + { + build: true, + service: 'Lambda', + resourceName: 'amplifyexportest075e4736', + category: 'function', + }, + { + service: 'LambdaLayer', + build: true, + resourceName: 'amplifyexportestlayer5f16d693', + category: 'function', + }, + { + service: 'Cognito', + resourceName: 'amplifyexportestf93dd5cb', + category: 'auth', + }, + { + service: 'AppSync', + resourceName: 'amplifyexportest', + category: 'api', + }, + ], + resourcesToBeUpdated: [], + resourcesToBeSynced: [], + resourcesToBeDeleted: [], + tagsUpdated: false, + allResources: [ + { + service: '', + resourceName: 'awscloudformation', + category: 'providers', + }, + { + build: true, + service: 'Lambda', + resourceName: 'amplifyexportest075e4736', + category: 'function', + providerPlugin: 'awscloudformation', + }, + { + service: 'LambdaLayer', + build: true, + resourceName: 'amplifyexportestlayer5f16d693', + category: 'function', + providerPlugin: 'awscloudformation', + }, + { + service: 'Cognito', + resourceName: 'amplifyexportestf93dd5cb', + category: 'auth', + providerPlugin: 'awscloudformation', + }, + { + service: 'Cognito-UserPool-Groups', + resourceName: 'userPoolGroups', + category: 'auth', + providerPlugin: 'awscloudformation', + }, + { + resourceName: 'containerf763043d', + build: true, + service: 'ElasticContainer', + category: 'api', + providerPlugin: 'awscloudformation', + }, + { + service: 'AppSync', + resourceName: 'amplifyexportest', + category: 'api', + providerPlugin: 'awscloudformation', + }, + ], +}; + +jest.mock('glob', () => ({ + sync: mockGlobSync, +})); + +const mockGlobSync = jest.fn((_, { cwd }) => [path.join(cwd, 'cfntemplate.json')]); +const lambdaTemplate = { + Resources: { + LambdaLayerVersionb8059db0: { + Type: 'AWS::Lambda::LayerVersion', + Properties: { + Content: { + S3Bucket: { + Ref: 'deploymentBucketName', + }, + S3Key: { + Ref: 's3Key', + }, + }, + }, + }, + LambdaLayerVersiond8833c37: { + Type: 'AWS::Lambda::LayerVersion', + Properties: { + Content: { + S3Bucket: { + Ref: 'deploymentBucketName', + }, + S3Key: 'amplify-builds/amplifyexportestlayera55b1f6d-LambdaLayerVersiond8833c37-build.zip', + }, + }, + }, + }, +}; + +const invokePluginMethod = jest.fn((_context, _category, _service, functionName, _others) => { + if (functionName === 'buildResource') { + return 'mockbuildTimeStamp'; + } + if (functionName === 'packageResource') { + return { + newPackageCreated: true, + zipFilename: 'mockZipFileName.zip', + zipFilePath: 'mockZipFilePath.zip', + }; + } + if (functionName === 'generateContainersArtifacts') { + return { + exposedContainer: 'mockExposedContainer', + }; + } +}); +jest.mock('../../resourceParams', () => ({ + loadResourceParameters: jest.fn().mockReturnValue({}), +})); +import path from 'path'; +import { ResourceExport } from '../../resource-package/resource-export'; +import { Template } from 'cloudform-types'; + +describe('test resource export', () => { + const exportPath = './exportPath'; + const mockContext = { + amplify: { + invokePluginMethod: invokePluginMethod, + getEnvInfo: stateManager_mock.getLocalEnvInfo, + getBackendDirPath: pathManager_mock.getBackendDirPath, + }, + input: { + command: 'notinit', + }, + parameters: { options: {} }, + exeInfo: { + localEnvInfo: { + envName: 'dev', + }, + }, + } as unknown as $TSContext; + + let resourceExport: ResourceExport; + + beforeAll(() => { + resourceExport = new ResourceExport(mockContext, exportPath); + }); + + let invokePluginCount: number = 1; + let packagedResources: PackagedResourceDefinition[] = []; + let exportStackParameters: StackParameters; + + test('resource Export is defined', async () => { + expect(resourceExport).toBeDefined(); + expect(resourceExport.deployType).toEqual(ResourceDeployType.Export); + expect(mockMeta).toBeCalledTimes(1); + }); + + test('resource Export build and write', async () => { + packagedResources = await resourceExport.packageBuildWriteResources(mockResource); + expect(packagedResources).not.toContain({ + service: '', + resourceName: 'awscloudformation', + category: 'providers', + }); + + const resourcesWithoutProvider = mockResource.allResources.filter(r => r.category !== 'providers'); + resourcesWithoutProvider + .filter(r => r.service === 'LambdaLayer') + .forEach(resource => { + expect(invokePluginMethod).nthCalledWith(invokePluginCount, mockContext, resource.category, 'LambdaLayer', 'migrateLegacyLayer', [ + mockContext, + resource.resourceName, + ]); + invokePluginCount++; + }); + + expect(invokePluginMethod).nthCalledWith(invokePluginCount, mockContext, 'function', 'LambdaLayer', 'lambdaLayerPrompt', [ + mockContext, + resourcesWithoutProvider, + ]); + invokePluginCount++; + + resourcesWithoutProvider + .filter(r => r.build) + .forEach(resource => { + expect(invokePluginMethod).nthCalledWith(invokePluginCount, mockContext, 'function', resource.service, 'buildResource', [ + mockContext, + resource, + ]); + invokePluginCount = invokePluginCount + 1; + }); + + const mockBuiltResources = resourcesWithoutProvider.map(resource => { + if (resource.build) { + return { + ...resource, + lastBuildTimeStamp: 'mockbuildTimeStamp', + }; + } + return resource; + }); + + mockBuiltResources + .filter(resource => resource.build) + .forEach(resource => { + expect(invokePluginMethod).nthCalledWith(invokePluginCount, mockContext, 'function', resource.service, 'packageResource', [ + mockContext, + resource, + true, + ]); + invokePluginCount = invokePluginCount + 1; + }); + + expect(mockTransformGql).toBeCalledTimes(1); + }); + + test('resource Export write resources to destination', async () => { + await resourceExport.writeResourcesToDestination(packagedResources); + + let copyCount = 1; + packagedResources.forEach(resource => { + if (resource.packagerParams) { + expect(fs_mock.copy).nthCalledWith( + copyCount++, + resource.packagerParams.zipFilePath, + path.join(exportPath, resource.category, resource.resourceName, 'amplify-builds', resource.packagerParams.zipFilename), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + } + + if (resource.service === 'LambdaLayer') { + expect(mockdownloadZip).toBeCalledWith( + mockS3Instance, + path.join(exportPath, resource.category, resource.resourceName), + 'amplify-builds/amplifyexportestlayera55b1f6d-LambdaLayerVersiond8833c37-build.zip', + 'dev', + ); + } + + if (resource.service === 'AppSync') { + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'build', 'functions'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-appsync-files', 'functions'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'build', 'pipelineFunctions'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-appsync-files', 'pipelineFunctions'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'build', 'resolvers'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-appsync-files', 'resolvers'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'build', 'stacks'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-appsync-files', 'stacks'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'build', 'schema.graphql'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-appsync-files', 'schema.graphql'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + } + + if (resource.service === 'Cognito') { + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join('backend', resource.category, resource.resourceName, 'assets'), + path.join(exportPath, resource.category, resource.resourceName, 'amplify-auth-assets'), + { overwrite: true, preserveTimestamps: true, recursive: true }, + ); + } + }); + + if (packagedResources.some(r => r.service === 'ElasticContainer')) { + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join(__dirname, '../../../', 'resources', 'custom-resource-pipeline-awaiter.zip'), + path.join(exportPath, 'amplify-auxiliary-files', 'custom-resource-pipeline-awaiter.zip'), + { + overwrite: true, + preserveTimestamps: true, + recursive: true, + }, + ); + expect(fs_mock.copy).nthCalledWith( + copyCount++, + path.join(__dirname, '../../../', 'resources', 'codepipeline-action-buildspec-generator-lambda.zip'), + path.join(exportPath, 'amplify-auxiliary-files', 'codepipeline-action-buildspec-generator-lambda.zip'), + { + overwrite: true, + preserveTimestamps: true, + recursive: true, + }, + ); + } + }); + + test('resource Export generate and transform cfn category resources', async () => { + const { stackParameters, transformedResources } = await resourceExport.generateAndTransformCfnResources(packagedResources); + exportStackParameters = stackParameters; + expect(stackParameters).toBeDefined(); + expect(transformedResources).toBeDefined(); + expect(mockconsolidateApiGatewayPolicies).toBeCalledWith(mockContext, 'amplify-amplifyexportest-dev-172019'); + + expect(invokePluginMethod).nthCalledWith(invokePluginCount++, mockContext, 'auth', undefined, 'prePushAuthHook', [mockContext]); + const apiResource = packagedResources.find(r => r.service === 'ElasticContainer'); + expect(invokePluginMethod).nthCalledWith(invokePluginCount++, mockContext, 'api', undefined, 'generateContainersArtifacts', [ + mockContext, + apiResource, + ]); + }); + + test('resource Export generate and write Root Stack', async () => { + const parameters = await resourceExport.generateAndWriteRootStack(exportStackParameters); + expect(Object.keys(parameters).length).toBeLessThan(2); + }); +}); diff --git a/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js b/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js index 571683df4b3..2b7d18faaab 100644 --- a/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js +++ b/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js @@ -311,6 +311,15 @@ class CloudFormation { }); } + async listStacks(nextToken = null, stackStatusFilter) { + return await this.cfn + .listStacks({ + NextToken: nextToken, + StackStatusFilter: stackStatusFilter, + }) + .promise(); + } + async updateamplifyMetaFileWithStackOutputs(parentStackName) { const cfnParentStackParams = { StackName: parentStackName, @@ -328,7 +337,7 @@ class CloudFormation { 'UpdateRolesWithIDPFunction', 'UpdateRolesWithIDPFunctionOutputs', 'UpdateRolesWithIDPFunctionRole', - ].includes(resource.LogicalResourceId), + ].includes(resource.LogicalResourceId) && resource.ResourceType === 'AWS::CloudFormation::Stack', ); if (resources.length > 0) { diff --git a/packages/amplify-provider-awscloudformation/src/export-resources.ts b/packages/amplify-provider-awscloudformation/src/export-resources.ts new file mode 100644 index 00000000000..d05329b21f7 --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/export-resources.ts @@ -0,0 +1,194 @@ +import { $TSAny, $TSContext, JSONUtilities, stateManager } from 'amplify-cli-core'; +import { ResourceExport } from './resource-package/resource-export'; +import { ResourceDefinition, StackIncludeDetails, StackParameters } from './resource-package/types'; +import * as path from 'path'; +import { printer, prompter } from 'amplify-prompts'; +import * as fs from 'fs-extra'; +import Ora from 'ora'; +const backup = 'backup'; +import _ from 'lodash'; +import rimraf from 'rimraf'; +import { validateExportDirectoryPath } from 'amplify-cli-core'; +// don't change file names ever ever +const AMPLIFY_EXPORT_MANIFEST_JSON_FILE = 'amplify-export-manifest.json'; +const AMPLIFY_EXPORT_TAGS_JSON_FILE = 'export-tags.json'; +const AMPLIFY_EXPORT_CATEGORY_STACK_MAPPING_FILE = 'category-stack-mapping.json'; +/** + * Walks through + * @param context + * @param resourceDefinition + * @param exportPath is the path to export to + */ +export async function run(context: $TSContext, resourceDefinition: $TSAny[], exportPath: string) { + validateExportDirectoryPath(exportPath); + + const { projectName } = stateManager.getProjectConfig(); + const amplifyExportFolder = path.join(path.resolve(exportPath), `amplify-export-${projectName}`); + const proceed = await checkForExistingExport(amplifyExportFolder); + + if (proceed) { + // create backup and then start exporting + await createBackup(amplifyExportFolder); + deleteFolder(amplifyExportFolder); + } else { + // return if user selects not to proceed + return; + } + + const spinner = Ora('Exporting...'); + spinner.start(); + try { + const resourceExport = new ResourceExport(context, amplifyExportFolder); + + spinner.text = 'Building and packaging resources'; + const packagedResources = await resourceExport.packageBuildWriteResources(resourceDefinition as any); + + spinner.text = `Writing resources`; + await resourceExport.writeResourcesToDestination(packagedResources); + + spinner.text = `Writing Cloudformation`; + const { stackParameters, transformedResources } = await resourceExport.generateAndTransformCfnResources(packagedResources); + + spinner.text = `Generating and writing root stack`; + const extractedParameters = await resourceExport.generateAndWriteRootStack(stackParameters); + const parameters = resourceExport.fixNestedStackParameters(transformedResources, extractedParameters); + + spinner.text = `Generating export manifest`; + writeExportManifest(parameters, exportPath, amplifyExportFolder); + + spinner.text = `Generating category stack mappings`; + createCategoryStackMapping(transformedResources, amplifyExportFolder); + + spinner.text = 'Generating export tag file'; + createTagsFile(amplifyExportFolder); + + spinner.text = 'Setting permissions'; + await setPermissions(amplifyExportFolder); + spinner.succeed('Done Exporting'); + printer.blankLine(); + printer.success('Successfully exported'); + printer.info('Some Next steps:'); + printer.info('You can now integrate your Amplify Backend into your CDK App'); + printer.info('By installing the Amplify Backend Export Construct by running npm i @aws-amplify/amplify-export-backend in your CDK app'); + printer.info('For more information: docs.amplify.aws/cli/export'); + printer.blankLine(); + } catch (ex) { + revertToBackup(amplifyExportFolder); + spinner.fail(); + throw ex; + } finally { + removeBackup(amplifyExportFolder); + spinner.stop(); + } +} + +/** + * setting permissions rwx for user + * @param amplifyExportFolder + */ +async function setPermissions(amplifyExportFolder: string): Promise { + await fs.chmod(amplifyExportFolder, 0o700); +} +/** + * Gets the tags from the tags.json file and transforms them into Pascal case + * leaves the project-env var in for the CDK construct to apply + * @param exportPath + */ +function createTagsFile(exportPath: string) { + const hydratedTags = stateManager.getHydratedTags(undefined, true); + + JSONUtilities.writeJson( + path.join(exportPath, AMPLIFY_EXPORT_TAGS_JSON_FILE), + hydratedTags.map(tag => ({ + key: tag.Key, + value: tag.Value, + })), + ); +} + +/** + * generates category stack mapping of the files + * @param resources + * @param amplifyExportFolder export folder different from the root of the export + */ +function createCategoryStackMapping(resources: ResourceDefinition[], amplifyExportFolder: string) { + JSONUtilities.writeJson( + path.join(amplifyExportFolder, AMPLIFY_EXPORT_CATEGORY_STACK_MAPPING_FILE), + resources.map(r => { + return _.pick(r, ['category', 'resourceName', 'service']); + }), + ); +} + +/** + * checks if there is an existing folder and prompt + * @param amplifyExportFolder + * @returns true if to proceed no if to exit + */ +async function checkForExistingExport(amplifyExportFolder: string): Promise { + let proceed = true; + if (fs.existsSync(amplifyExportFolder)) { + proceed = await prompter.yesOrNo( + `Existing files at ${amplifyExportFolder} will be deleted and new files will be generated, continue?`, + true, + ); + } + await fs.ensureDir(amplifyExportFolder); + return proceed; +} + +function deleteFolder(directoryPath) { + if (fs.existsSync(directoryPath)) { + rimraf.sync(directoryPath); + } +} + +async function removeBackup(amplifyExportFolder: string) { + if (fs.existsSync(`${amplifyExportFolder}-${backup}`)) { + deleteFolder(`${amplifyExportFolder}-${backup}`); + } +} +async function revertToBackup(amplifyExportFolder: string) { + if (fs.existsSync(`${amplifyExportFolder}-${backup}`)) { + await fs.copy(`${amplifyExportFolder}-${backup}`, amplifyExportFolder); + } +} + +async function createBackup(amplifyExportFolder: string) { + await fs.copy(amplifyExportFolder, `${amplifyExportFolder}-${backup}`); +} + +/** + * Transforms the stackparameters file path to convert into the export manifest file + * @param stackParameters + * @param exportPath + * @param amplifyExportFolder + */ +function writeExportManifest(stackParameters: StackParameters, exportPath: string, amplifyExportFolder: string) { + const rootStackParametersKey = _.first(Object.keys(stackParameters)); + const manifestJson = { + stackName: rootStackParametersKey, + props: transformManifestParameters(stackParameters[rootStackParametersKey], exportPath), + }; + JSONUtilities.writeJson(path.join(amplifyExportFolder, AMPLIFY_EXPORT_MANIFEST_JSON_FILE), manifestJson); +} + +function transformManifestParameters(stackParameters: StackIncludeDetails, exportPath: string) { + if (stackParameters) { + const manifest = { + templateFile: path.relative(exportPath, stackParameters.destination), + parameters: stackParameters.parameters, + preserveLogicalIds: true, + loadNestedStacks: {}, + }; + if (!stackParameters.nestedStacks) { + return manifest; + } + Object.keys(stackParameters.nestedStacks) + .sort() + .forEach(key => { + manifest['loadNestedStacks'][key] = transformManifestParameters(stackParameters.nestedStacks[key], exportPath); + }); + return manifest; + } +} diff --git a/packages/amplify-provider-awscloudformation/src/export-types/BuiltResourceType.ts b/packages/amplify-provider-awscloudformation/src/export-types/BuiltResourceType.ts new file mode 100644 index 00000000000..3c1f4e0026d --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/export-types/BuiltResourceType.ts @@ -0,0 +1,10 @@ +import { ResourceType } from './ResourceType'; + +export type BuiltParams = { + zipFilename: string; + zipFilePath: string; +}; + +export type BuiltResourceType = ResourceType & { + buildParams?: BuiltParams[]; +}; diff --git a/packages/amplify-provider-awscloudformation/src/export-types/ResourceType.ts b/packages/amplify-provider-awscloudformation/src/export-types/ResourceType.ts new file mode 100644 index 00000000000..e998c0688c8 --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/export-types/ResourceType.ts @@ -0,0 +1,6 @@ +export type ResourceType = { + category: string; + service: string; + resourceName: string; + build: boolean; +}; diff --git a/packages/amplify-provider-awscloudformation/src/export-update-amplify-meta.ts b/packages/amplify-provider-awscloudformation/src/export-update-amplify-meta.ts new file mode 100644 index 00000000000..7848a7f4887 --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/export-update-amplify-meta.ts @@ -0,0 +1,46 @@ +import { $TSContext, ExportedStackNotFoundError, ExportedStackNotInValidStateError } from 'amplify-cli-core'; +import Cloudformation from './aws-utils/aws-cfn'; +import { printer } from 'amplify-prompts'; +import * as _ from 'lodash'; + +export async function run(context: $TSContext, stackName: string) { + const cfn = await new Cloudformation(context); + let rootStack = null; + let nextToken = null; + let continueListing = false; + do { + const stacks = await cfn.listStacks(nextToken, []); + rootStack = _.find(stacks.StackSummaries, summary => summary.StackName === stackName); + // if stack found the + if (rootStack) { + continueListing = false; + continue; + } + + // if reached the end then stop listing + if (!stacks.NextToken) { + continueListing = false; + continue; + } + + // if the stack is not found keep looking + if (stacks.NextToken) { + nextToken = stacks.NextToken; + continueListing = true; + continue; + } + } while (continueListing); + + // if stack isn't found mostly because the stack isn't accessible by the credentials + if (!rootStack) { + printer.error(`${stackName} could not be found, are you sure you are using the right credentials?`); + throw new ExportedStackNotFoundError(`${stackName} not found`); + } + + // if the stack is found and is not in valid state + if (rootStack.StackStatus !== 'UPDATE_COMPLETE' && rootStack.StackStatus !== 'CREATE_COMPLETE') { + throw new ExportedStackNotInValidStateError(`${stackName} not in UPDATE_COMPLETE or CREATE_COMPLETE state`); + } + + await cfn.updateamplifyMetaFileWithStackOutputs(stackName); +} diff --git a/packages/amplify-provider-awscloudformation/src/index.ts b/packages/amplify-provider-awscloudformation/src/index.ts index d90f3de0ece..179ee7d364a 100644 --- a/packages/amplify-provider-awscloudformation/src/index.ts +++ b/packages/amplify-provider-awscloudformation/src/index.ts @@ -30,6 +30,8 @@ import { SSM } from './aws-utils/aws-ssm'; import { Lambda } from './aws-utils/aws-lambda'; import CloudFormation from './aws-utils/aws-cfn'; import { $TSContext } from 'amplify-cli-core'; +import * as resourceExport from './export-resources'; +import * as exportUpdateMeta from './export-update-amplify-meta'; export { resolveAppId } from './utils/resolve-appId'; export { loadConfigurationForEnv } from './configuration-manager'; @@ -65,8 +67,18 @@ function onInitSuccessful(context) { return initializer.onInitSuccessful(context); } -function pushResources(context, resourceList, rebuild: boolean = false) { - return resourcePusher.run(context, resourceList, rebuild); + +function exportResources(context, resourceList, exportType) { + return resourceExport.run(context, resourceList, exportType); +} + +function exportedStackResourcesUpdateMeta(context: $TSContext, stackName: string) { + return exportUpdateMeta.run(context, stackName); +} + + +function pushResources(context, resourceList) { + return resourcePusher.run(context, resourceList); } function storeCurrentCloudBackend(context) { @@ -133,6 +145,8 @@ module.exports = { adminLoginFlow, console: openConsole, attachBackend, + exportResources, + exportedStackResourcesUpdateMeta, init, initEnv, isAmplifyAdminApp, diff --git a/packages/amplify-provider-awscloudformation/src/push-resources.ts b/packages/amplify-provider-awscloudformation/src/push-resources.ts index db14231ffd9..9323891fd6d 100644 --- a/packages/amplify-provider-awscloudformation/src/push-resources.ts +++ b/packages/amplify-provider-awscloudformation/src/push-resources.ts @@ -94,8 +94,8 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject, re const { parameters: { options }, } = context; - let resources = !!context?.exeInfo?.forcePush || rebuild ? allResources : resourcesToBeCreated.concat(resourcesToBeUpdated); + layerResources = resources.filter(r => r.service === FunctionServiceNameLambdaLayer); if (deploymentStateManager.isDeploymentInProgress() && !deploymentStateManager.isDeploymentFinished()) { @@ -122,6 +122,7 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject, re resources = _.uniqBy(resources.concat(functionResourceToBeUpdated), `resourceName`); } } + validateCfnTemplates(context, resources); for (const resource of resources) { @@ -760,7 +761,7 @@ function getAllUniqueCategories(resources: $TSObject[]): $TSObject[] { return [...categories]; } -function getCfnFiles(category: string, resourceName: string) { +export function getCfnFiles(category: string, resourceName: string, options?: glob.IOptions) { const backEndDir = pathManager.getBackendDirPath(); const resourceDir = path.normalize(path.join(backEndDir, category, resourceName)); const resourceBuildDir = path.join(resourceDir, optionalBuildDirectoryName); @@ -774,6 +775,7 @@ function getCfnFiles(category: string, resourceName: string) { const cfnFiles = glob.sync(cfnTemplateGlobPattern, { cwd: resourceBuildDir, ignore: [parametersJson], + ...options, }); if (cfnFiles.length > 0) { @@ -787,6 +789,7 @@ function getCfnFiles(category: string, resourceName: string) { const cfnFiles = glob.sync(cfnTemplateGlobPattern, { cwd: resourceDir, ignore: [parametersJson, AUTH_TRIGGER_TEMPLATE], + ...options, }); return { @@ -853,7 +856,7 @@ export async function uploadTemplateToS3( } } -async function formNestedStack( +export async function formNestedStack( context: $TSContext, projectDetails: $TSObject, categoryName?: string, @@ -906,7 +909,7 @@ async function formNestedStack( let authResourceName: string; const { APIGatewayAuthURL, NetworkStackS3Url, AuthTriggerTemplateURL } = amplifyMeta.providers[constants.ProviderName]; - + const { envName } = stateManager.getLocalEnvInfo(); if (APIGatewayAuthURL) { const stack = { Type: 'AWS::CloudFormation::Stack', @@ -919,7 +922,7 @@ async function formNestedStack( unauthRoleName: { Ref: 'UnauthRoleName', }, - env: context.exeInfo.localEnvInfo.envName, + env: envName, }, }, }; @@ -944,7 +947,7 @@ async function formNestedStack( Properties: { TemplateURL: AuthTriggerTemplateURL, Parameters: { - env: context.exeInfo.localEnvInfo.envName, + env: envName, }, }, DependsOn: [], @@ -1169,6 +1172,14 @@ function updateIdPRolesInNestedStack(nestedStack: $TSAny, authResourceName: $TSA Object.assign(nestedStack.Resources, idpUpdateRoleCfn); } +function isAuthTrigger(dependsOnResource: $TSObject) { + return ( + FeatureFlags.getBoolean('auth.breakCircularDependency') && + dependsOnResource.category === 'function' && + dependsOnResource.triggerProvider === 'Cognito' + ); +} + export async function generateAndUploadRootStack(context: $TSContext, destinationPath: string, destinationS3Key: string) { const projectDetails = context.amplify.getProjectDetails(); const nestedStack = await formNestedStack(context, projectDetails); @@ -1185,14 +1196,6 @@ export async function generateAndUploadRootStack(context: $TSContext, destinatio await s3Client.uploadFile(s3Params, false); } -function isAuthTrigger(dependsOnResource: $TSObject) { - return ( - FeatureFlags.getBoolean('auth.breakCircularDependency') && - dependsOnResource.category === 'function' && - dependsOnResource.triggerProvider === 'Cognito' - ); -} - function rollbackLambdaLayers(layerResources: $TSAny[]) { if (layerResources.length > 0) { const projectRoot = pathManager.findProjectRoot(); diff --git a/packages/amplify-provider-awscloudformation/src/resource-package/constants.ts b/packages/amplify-provider-awscloudformation/src/resource-package/constants.ts new file mode 100644 index 00000000000..864785de003 --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/resource-package/constants.ts @@ -0,0 +1,59 @@ +export const Constants = { + PROVIDER_NAME: 'awscloudformation', + PROVIDER: 'providers', + AMPLIFY_BUILDS: 'amplify-builds', + CATEGORIES: 'categories', + S3_BUCKET: 's3Bucket', + AMPLIFY_CFN_TEMPLATES: 'amplify-cfn-templates', + EXPOSED_CONTAINER: 'exposedContainer', + PARAMETERS_JSON_FILE: 'parameters.json', + CFN_TEMPLATE_GLOB_PATTERN: '*template*.+(yaml|yml|json)', + PROVIDER_METADATA: 'providerMetadata', + NETWORK_STACK_S3_URL: 'NetworkStackS3Url', + NETWORK_STACK_FILENAME: 'networkingStackTemplate.json', + NETWORK_STACK_LOGICAL_ID: 'NetworkStack', + API_GATEWAY_AUTH_URL: 'APIGatewayAuthURL', + AUTH_TRIGGER_TEMPLATE_FILE: 'auth-trigger-cloudformation-template.json', + AUTH_TRIGGER_TEMPLATE_URL: 'AuthTriggerTemplateURL', + AUTH_TRIGGER_STACK: 'AuthTriggerCustomLambdaStack', + APIGW_AUTH_STACK_LOGICAL_ID: 'APIGatewayAuthStack', + APIGW_AUTH_STACK_FILE_NAME: 'APIGatewayAuthStack.json', + AWS_CLOUDFORMATION_STACK_TYPE: 'AWS::CloudFormation::Stack', + EXPORT_DEPLOYMENT_BUCKET_NAME: 'exportDeploymentBucket', + AMPLIFY_AUXILIARY_LAMBDAS: 'amplify-auxiliary-files', + APPSYNC_BUILD_FOLDER: 'build', + APPSYNC_STACK_FOLDER: 'stacks', + AUTH_ASSETS: 'assets', + AMPLIFY_AUTH_ASSETS: 'amplify-auth-assets', + AMPLIFY_APPSYNC_FILES: 'amplify-appsync-files', + NOTIFICATIONS_CATEGORY: { + NAME: 'notifications', + }, + API_CATEGORY: { + NAME: 'api', + SERVICE: { + APP_SYNC: 'AppSync', + ELASTIC_CONTAINER: 'ElasticContainer', + API_GATEWAY: 'API Gateway', + }, + }, + HOSTING_CATEGORY: { + NAME: 'hosting', + SERVICE: { + ELASTIC_CONTAINER: 'ElasticContainer', + }, + }, + FUNCTION_CATEGORY: { + NAME: 'function', + SERVICE: { + LAMBDA_FUNCTION: 'Lambda', + LAMBDA_LAYER: 'LambdaLayer', + }, + }, + AUTH_CATEGORY: { + NAME: 'auth', + SERVICE: { + COGNITO: 'Cognito', + }, + }, +}; diff --git a/packages/amplify-provider-awscloudformation/src/resource-package/resource-export.ts b/packages/amplify-provider-awscloudformation/src/resource-package/resource-export.ts new file mode 100644 index 00000000000..242c0b1c7dc --- /dev/null +++ b/packages/amplify-provider-awscloudformation/src/resource-package/resource-export.ts @@ -0,0 +1,464 @@ +import { + $TSContext, + CFNTemplateFormat, + FeatureFlags, + JSONUtilities, + pathManager, + readCFNTemplate, + stateManager, + writeCFNTemplate, +} from 'amplify-cli-core'; +import { Template, Fn } from 'cloudform-types'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { + DeploymentResources, + PackagedResourceDefinition, + ResourceDefinition, + ResourceDeployType, + StackParameters, + TransformedCfnResource, +} from './types'; +import { Constants } from './constants'; +import { ResourcePackager } from './resource-packager'; +import { getNetworkResourceCfn } from '../utils/env-level-constructs'; +import _ from 'lodash'; +import { printer } from 'amplify-prompts'; +import { AUTH_TRIGGER_STACK } from '../utils/upload-auth-trigger-template'; +import { S3 } from '../aws-utils/aws-s3'; +import { downloadZip } from '../zip-util'; +import { Ref } from 'cloudform-types/types/functions'; +const { + API_CATEGORY, + AUTH_CATEGORY, + FUNCTION_CATEGORY, + NOTIFICATIONS_CATEGORY, + AMPLIFY_CFN_TEMPLATES, + AMPLIFY_APPSYNC_FILES, + PROVIDER_METADATA, + NETWORK_STACK_S3_URL, + AUTH_TRIGGER_TEMPLATE_FILE, + AUTH_TRIGGER_TEMPLATE_URL, + API_GATEWAY_AUTH_URL, + APIGW_AUTH_STACK_FILE_NAME, + APPSYNC_STACK_FOLDER, + APPSYNC_BUILD_FOLDER, + NETWORK_STACK_FILENAME, + PROVIDER_NAME, + PROVIDER, + AMPLIFY_BUILDS, + AUTH_ASSETS, + AMPLIFY_AUXILIARY_LAMBDAS, + AWS_CLOUDFORMATION_STACK_TYPE, + AMPLIFY_AUTH_ASSETS, + NETWORK_STACK_LOGICAL_ID, + APIGW_AUTH_STACK_LOGICAL_ID, +} = Constants; +export class ResourceExport extends ResourcePackager { + exportDirectoryPath: string; + constructor(context: $TSContext, exportDirectoryPath: string) { + super(context, ResourceDeployType.Export); + this.exportDirectoryPath = exportDirectoryPath; + } + + async packageBuildWriteResources(deploymentResources: DeploymentResources): Promise { + this.warnForNonExportable(deploymentResources.allResources); + const resources = await this.filterResourcesToBeDeployed(deploymentResources); + + const preBuiltResources = await this.preBuildResources(resources); + const builtResources = await this.buildResources(preBuiltResources); + const packagedResources = await this.packageResources(builtResources); + const postPackageResources = await this.postPackageResource(packagedResources); + return postPackageResources; + } + + async generateAndTransformCfnResources( + packagedResources: PackagedResourceDefinition[], + ): Promise<{ transformedResources: TransformedCfnResource[]; stackParameters: StackParameters }> { + await this.generateCategoryCloudFormation(packagedResources); + const transformedCfnResources = await this.postGenerateCategoryCloudFormation(packagedResources); + const stackParameters = await this.writeCategoryCloudFormation(transformedCfnResources); + return { transformedResources: transformedCfnResources, stackParameters }; + } + + /** + * The parameters are going to need to be read from the parameters json since the types are fixed there + * @param transformedCfnResources + * @param stackParameters + * @returns + */ + fixNestedStackParameters(transformedCfnResources: TransformedCfnResource[], stackParameters: StackParameters): StackParameters { + const projectPath = pathManager.findProjectRoot(); + const { StackName: rootstackName } = this.amplifyMeta[PROVIDER][PROVIDER_NAME]; + const nestedStack = stackParameters[rootstackName].nestedStacks; + for (const resource of transformedCfnResources) { + const fileParameters = stateManager.getResourceParametersJson(projectPath, resource.category, resource.resourceName, { + default: {}, + throwIfNotExist: false, + }); + const nestedStackName = resource.category + resource.resourceName; + const usedParameters = nestedStack[nestedStackName].parameters; + Object.keys(usedParameters).forEach(paramKey => { + if (paramKey in fileParameters) { + usedParameters[paramKey] = fileParameters[paramKey]; + } + }); + } + return stackParameters; + } + + async generateAndWriteRootStack(stackParameters: StackParameters): Promise { + const { StackName: stackName, AuthRoleName, UnauthRoleName, DeploymentBucketName } = this.amplifyMeta[PROVIDER][PROVIDER_NAME]; + const template = await this.generateRootStack(); + const parameters = this.extractParametersFromTemplateNestedStack(template); + const modifiedTemplate = this.modifyRootStack(template, true); + this.writeRootStackToPath(modifiedTemplate); + stackParameters[stackName].destination = path.join(this.exportDirectoryPath, 'root-stack-template.json'); + + [...parameters.keys()].forEach((key: string) => { + if (stackParameters[stackName].nestedStacks && stackParameters[stackName].nestedStacks[key]) { + stackParameters[stackName].nestedStacks[key].parameters = parameters.get(key); + } + }); + + stackParameters[stackName].parameters = { + AuthRoleName, + UnauthRoleName, + DeploymentBucketName, + }; + + return stackParameters; + } + /** + * warns for non exportable resources + * @param resources + */ + warnForNonExportable(resources: ResourceDefinition[]) { + const notificationsResources = this.filterResourceByCategoryService(resources, NOTIFICATIONS_CATEGORY.NAME); + if (notificationsResources.length > 0) { + printer.blankLine(); + printer.warn( + `The ${NOTIFICATIONS_CATEGORY.NAME} resource '${notificationsResources + .map(r => r.resourceName) + .join(', ')}' cannot be exported since it is managed using SDK`, + ); + + printer.warn(`Please refer to documentation to reference the resource here manually `); + } + } + + /** + * writes packaged files to export directory path + * For AppSync API it copies the non cloudformation assets + * + * @param resources {PackagedResourceDefinition[]} + */ + async writeResourcesToDestination(resources: PackagedResourceDefinition[]): Promise { + for (const resource of resources) { + if (resource.packagerParams && resource.packagerParams.newPackageCreated) { + const destinationPath = path.join( + this.exportDirectoryPath, + resource.category, + resource.resourceName, + AMPLIFY_BUILDS, + resource.packagerParams.zipFilename, + ); + await this.copyResource(resource.packagerParams.zipFilePath, destinationPath); + } + if (resource.category === FUNCTION_CATEGORY.NAME && resource.service === FUNCTION_CATEGORY.SERVICE.LAMBDA_LAYER) { + await this.downloadLambdaLayerContent(resource); + } + // copy build directory for appsync + if (resource.category === API_CATEGORY.NAME && resource.service === API_CATEGORY.SERVICE.APP_SYNC) { + const backendFolder = pathManager.getBackendDirPath(); + const foldersToCopy = ['functions', 'pipelineFunctions', 'resolvers', 'stacks', 'schema.graphql']; + for (const folder of foldersToCopy) { + const sourceFolder = path.join(backendFolder, resource.category, resource.resourceName, APPSYNC_BUILD_FOLDER, folder); + const destinationFolder = path.join( + this.exportDirectoryPath, + resource.category, + resource.resourceName, + AMPLIFY_APPSYNC_FILES, + folder, + ); + await this.copyResource(sourceFolder, destinationFolder); + } + } + + if (resource.category === AUTH_CATEGORY.NAME && resource.service === AUTH_CATEGORY.SERVICE.COGNITO) { + const authResourceBackend = path.join(pathManager.getBackendDirPath(), resource.category, resource.resourceName); + const authResourceAssets = path.join(authResourceBackend, AUTH_ASSETS); + if (fs.existsSync(authResourceAssets)) { + const destinationPath = path.join(this.exportDirectoryPath, resource.category, resource.resourceName, AMPLIFY_AUTH_ASSETS); + await this.copyResource(authResourceAssets, destinationPath); + } + } + } + // write the pipeline awaiter and + if (this.resourcesHasContainers(resources)) { + for (const zipFile of this.elasticContainerZipFiles) { + const destinationPath = path.join(this.exportDirectoryPath, AMPLIFY_AUXILIARY_LAMBDAS, zipFile); + const sourceFile = path.join(__dirname, '../..', 'resources', zipFile); + await this.copyResource(sourceFile, destinationPath); + } + } + } + /** + * Download content for past layer versions + * @param resource + */ + private async downloadLambdaLayerContent(resource: PackagedResourceDefinition) { + const backendDir = pathManager.getBackendDirPath(); + const cfnFilePath = path.join( + backendDir, + resource.category, + resource.resourceName, + `${resource.resourceName}-awscloudformation-template.json`, + ); + const template = JSONUtilities.readJson