From d09a1f2d8515f5e89a5b74f272d5d7de12dc0922 Mon Sep 17 00:00:00 2001 From: Attila Hajdrik Date: Fri, 25 Sep 2020 10:44:16 -0700 Subject: [PATCH 1/5] fix: add support for mobile hub migrated resources - support push/pull operations - support CRUD permission assignment to migrated resources - block multi-env commands - block update and remove operations on migrated resources --- packages/amplify-category-analytics/index.js | 5 +- packages/amplify-category-api/src/index.ts | 12 +- .../service-walkthroughs/apigw-walkthrough.ts | 4 +- packages/amplify-category-auth/package.json | 1 + packages/amplify-category-auth/src/index.js | 5 +- packages/amplify-category-auth/tsconfig.json | 3 +- .../amplify-category-function/src/index.ts | 5 +- .../execPermissionsWalkthrough.ts | 6 + .../lambda-walkthrough.ts | 2 +- .../commands/hosting/remove.js | 4 +- .../amplify-category-interactions/index.js | 9 +- .../service-walkthroughs/lex-walkthrough.js | 2 +- .../commands/notifications/configure.js | 5 +- .../commands/notifications/remove.js | 7 + .../lib/multi-env-manager.js | 76 +++++---- .../lib/notifications-manager.js | 8 + packages/amplify-category-storage/index.js | 8 +- .../dynamoDb-walkthrough.js | 2 +- .../service-walkthroughs/s3-walkthrough.js | 2 +- .../amplify-helpers/remove-resource.ts | 6 +- packages/amplify-cli/src/index.ts | 19 ++- packages/amplify-cli/src/migrate-project.ts | 2 +- .../src/utils/mobilehub-support.ts | 48 ++++++ .../src/attach-backend.js | 16 +- .../src/aws-utils/aws-cfn.js | 118 +++++++------- .../src/initialize-env.js | 146 +++++++++++------- 26 files changed, 340 insertions(+), 181 deletions(-) create mode 100644 packages/amplify-cli/src/utils/mobilehub-support.ts diff --git a/packages/amplify-category-analytics/index.js b/packages/amplify-category-analytics/index.js index 8a340caece0..c0dc897f95a 100644 --- a/packages/amplify-category-analytics/index.js +++ b/packages/amplify-category-analytics/index.js @@ -50,8 +50,9 @@ async function getPermissionPolicies(context, resourceOpsMapping) { Object.keys(resourceOpsMapping).forEach(resourceName => { try { - const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); - if (providerController) { + const providerName = amplifyMeta[category][resourceName].providerPlugin; + if (providerName) { + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, diff --git a/packages/amplify-category-api/src/index.ts b/packages/amplify-category-api/src/index.ts index 78b09b52053..3c8664f6874 100644 --- a/packages/amplify-category-api/src/index.ts +++ b/packages/amplify-category-api/src/index.ts @@ -58,6 +58,13 @@ export async function initEnv(context) { * configured an RDS datasource */ const backendConfigFilePath = amplify.pathManager.getBackendConfigFilePath(); + + // If this is a mobile hub migrated project without locally added resources then there is no + // backend config exists yet. + if (!fs.existsSync(backendConfigFilePath)) { + return; + } + const backendConfig = amplify.readJsonFile(backendConfigFilePath); if (!backendConfig[category]) { @@ -146,8 +153,9 @@ export async function getPermissionPolicies(context, resourceOpsMapping) { await Promise.all( Object.keys(resourceOpsMapping).map(async resourceName => { try { - const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); - if (providerController) { + const providerName = amplifyMeta[category][resourceName].providerPlugin; + if (providerName) { + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = await providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts index d0373f2f1bb..4a09d0e9a55 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts @@ -34,7 +34,9 @@ export async function updateWalkthrough(context, defaultValuesFilename) { const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; const { getAllDefaults } = await import(defaultValuesSrc); const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); - const resources = allResources.filter(resource => resource.service === serviceName).map(resource => resource.resourceName); + const resources = allResources + .filter(resource => resource.service === serviceName && !!resource.providerPlugin) + .map(resource => resource.resourceName); // There can only be one appsync resource if (resources.length === 0) { diff --git a/packages/amplify-category-auth/package.json b/packages/amplify-category-auth/package.json index 2a6534a3901..ec08439d7f4 100644 --- a/packages/amplify-category-auth/package.json +++ b/packages/amplify-category-auth/package.json @@ -24,6 +24,7 @@ "amplify-category-function": "2.25.0", "amplify-cli-core": "1.3.2", "amplify-headless-interface": "1.3.0", + "amplify-util-headless-input": "1.2.1", "aws-sdk": "^2.608.0", "chalk": "^3.0.0", "chalk-pipe": "^3.0.0", diff --git a/packages/amplify-category-auth/src/index.js b/packages/amplify-category-auth/src/index.js index 25179cfa692..c5f8f195b51 100644 --- a/packages/amplify-category-auth/src/index.js +++ b/packages/amplify-category-auth/src/index.js @@ -268,8 +268,9 @@ async function getPermissionPolicies(context, resourceOpsMapping) { Object.keys(resourceOpsMapping).forEach(resourceName => { try { - const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); - if (providerController) { + const providerName = amplifyMeta[category][resourceName].providerPlugin; + if (providerName) { + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, diff --git a/packages/amplify-category-auth/tsconfig.json b/packages/amplify-category-auth/tsconfig.json index 3236ff02f3c..302fe35aee3 100644 --- a/packages/amplify-category-auth/tsconfig.json +++ b/packages/amplify-category-auth/tsconfig.json @@ -13,7 +13,8 @@ "src/__tests__", ], "references": [ + {"path": "../amplify-cli-core"}, {"path": "../amplify-headless-interface"}, - {"path": "../amplify-cli-core"} + {"path": "../amplify-util-headless-input"}, ] } diff --git a/packages/amplify-category-function/src/index.ts b/packages/amplify-category-function/src/index.ts index 88b4c2cab5f..75e0f650792 100644 --- a/packages/amplify-category-function/src/index.ts +++ b/packages/amplify-category-function/src/index.ts @@ -73,8 +73,9 @@ export async function getPermissionPolicies(context, resourceOpsMapping) { Object.keys(resourceOpsMapping).forEach(resourceName => { try { - const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); - if (providerController) { + const providerName = amplifyMeta[category][resourceName].providerPlugin; + if (providerName) { + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/execPermissionsWalkthrough.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/execPermissionsWalkthrough.ts index 8ce7b0c08b6..d42ef63bfaf 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/execPermissionsWalkthrough.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/execPermissionsWalkthrough.ts @@ -113,6 +113,12 @@ export const askExecRolePermissionsQuestions = async ( if (!getPermissionPolicies) { context.print.warning(`Policies cannot be added for ${category}/${resourceName}`); continue; + } else if ( + amplifyMeta[category][resourceName].service === 'S3AndCloudFront' && + !amplifyMeta[category][resourceName].providerPlugin + ) { + context.print.warning(`Policies cannot be added for ${category}/${resourceName}, since it is a MobileHub imported resource.`); + continue; } else { const crudPermissionQuestion = { type: 'checkbox', diff --git a/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.ts b/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.ts index ffaa7601086..721b5c172c6 100644 --- a/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.ts +++ b/packages/amplify-category-function/src/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.ts @@ -59,7 +59,7 @@ export async function createWalkthrough( */ export async function updateWalkthrough(context, lambdaToUpdate?: string) { const lambdaFuncResourceNames = ((await context.amplify.getResourceStatus()).allResources as any[]) - .filter(resource => resource.service === ServiceName.LambdaFunction) + .filter(resource => resource.service === ServiceName.LambdaFunction && !!resource.providerPlugin) .map(resource => resource.resourceName); if (lambdaFuncResourceNames.length === 0) { diff --git a/packages/amplify-category-hosting/commands/hosting/remove.js b/packages/amplify-category-hosting/commands/hosting/remove.js index 0e80e1368b9..5e906364a53 100644 --- a/packages/amplify-category-hosting/commands/hosting/remove.js +++ b/packages/amplify-category-hosting/commands/hosting/remove.js @@ -39,8 +39,8 @@ async function chooseResource(context, inputResourceName) { const amplifyMetaFilePath = amplify.pathManager.getAmplifyMetaFilePath(); if (fs.existsSync(amplifyMetaFilePath)) { const amplifyMeta = amplify.readJsonFile(amplifyMetaFilePath); - if (amplifyMeta[category] && Object.keys(amplifyMeta[category]).length > 0) { - let enabledResources = Object.keys(amplifyMeta[category]); + if (amplifyMeta[category] && Object.keys(amplifyMeta[category]).filter(r => !!amplifyMeta[category][r].providerPlugin).length > 0) { + let enabledResources = Object.keys(amplifyMeta[category]).filter(r => !!amplifyMeta[category][r].providerPlugin); let inputIsValid = true; if (services && services.length > 0) { diff --git a/packages/amplify-category-interactions/index.js b/packages/amplify-category-interactions/index.js index 58a44e1d935..c1a441f0794 100644 --- a/packages/amplify-category-interactions/index.js +++ b/packages/amplify-category-interactions/index.js @@ -12,7 +12,7 @@ async function migrate(context) { const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); if (providerController) { migrateResourcePromises.push( - providerController.migrateResource(context, projectPath, amplifyMeta[category][resourceName].service, resourceName) + providerController.migrateResource(context, projectPath, amplifyMeta[category][resourceName].service, resourceName), ); } else { context.print.error(`Provider not configured for ${category}: ${resourceName}`); @@ -36,13 +36,14 @@ async function getPermissionPolicies(context, resourceOpsMapping) { Object.keys(resourceOpsMapping).forEach(resourceName => { try { - const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); - if (providerController) { + const providerName = amplifyMeta[category][resourceName].providerPlugin; + if (providerName) { + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, resourceName, - resourceOpsMapping[resourceName] + resourceOpsMapping[resourceName], ); permissionPolicies.push(policy); resourceAttributes.push({ resourceName, attributes, category }); diff --git a/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js b/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js index dd07c22d33a..c2fcca43073 100644 --- a/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js +++ b/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js @@ -22,7 +22,7 @@ function updateWalkthrough(context, defaultValuesFilename, serviceMetadata) { const lexResources = {}; Object.keys(amplifyMeta[category]).forEach(resourceName => { - if (amplifyMeta[category][resourceName].service === serviceName) { + if (amplifyMeta[category][resourceName].service === serviceName && !!amplifyMeta[category][resourceName].providerPlugin) { lexResources[resourceName] = amplifyMeta[category][resourceName]; } }); diff --git a/packages/amplify-category-notifications/commands/notifications/configure.js b/packages/amplify-category-notifications/commands/notifications/configure.js index 5957bee936b..f1c71bfb1d9 100644 --- a/packages/amplify-category-notifications/commands/notifications/configure.js +++ b/packages/amplify-category-notifications/commands/notifications/configure.js @@ -23,8 +23,9 @@ module.exports = { } await pinpointHelper.ensurePinpointApp(context); - await notificationManager.configureChannel(context, channelName); - await multiEnvManager.writeData(context); + if (await notificationManager.configureChannel(context, channelName)) { + await multiEnvManager.writeData(context); + } return context; }, diff --git a/packages/amplify-category-notifications/commands/notifications/remove.js b/packages/amplify-category-notifications/commands/notifications/remove.js index c7c8a023998..1cf93e02381 100644 --- a/packages/amplify-category-notifications/commands/notifications/remove.js +++ b/packages/amplify-category-notifications/commands/notifications/remove.js @@ -13,6 +13,13 @@ module.exports = { context.exeInfo = context.amplify.getProjectDetails(); const pinpointApp = pinpointHelper.getPinpointApp(context); if (pinpointApp) { + const pinpointResource = context.exeInfo.amplifyMeta.notifications[pinpointApp.Name]; + + if (pinpointResource && !pinpointResource.providerPlugin) { + context.print.error('Notifications is migrated from Mobile Hub and cannot be removed with Amplify CLI.'); + return context; + } + const availableChannels = notificationManager.getAvailableChannels(context); const enabledChannels = notificationManager.getEnabledChannels(context); diff --git a/packages/amplify-category-notifications/lib/multi-env-manager.js b/packages/amplify-category-notifications/lib/multi-env-manager.js index 1833ad1c8a1..4aa06e29a10 100644 --- a/packages/amplify-category-notifications/lib/multi-env-manager.js +++ b/packages/amplify-category-notifications/lib/multi-env-manager.js @@ -34,43 +34,67 @@ async function constructPinpointNotificationsMeta(context) { pinpointApp = teamProviderInfo[envName].categories[constants.CategoryName][constants.PinpointName]; } + let isMobileHubMigrated = false; + if (!pinpointApp) { const analyticsMeta = amplifyMeta[constants.AnalyticsCategoryName]; - pinpointApp = pinpointHelper.scanCategoryMetaForPinpoint(analyticsMeta); - } - const backendConfigFilePath = context.amplify.pathManager.getBackendConfigFilePath(); - const backendConfig = context.amplify.readJsonFile(backendConfigFilePath); - if (backendConfig[constants.CategoryName]) { - const categoryConfig = backendConfig[constants.CategoryName]; - const resources = Object.keys(categoryConfig); - for (let i = 0; i < resources.length; i++) { - serviceBackendConfig = categoryConfig[resources[i]]; - if (serviceBackendConfig.service === constants.PinpointName) { - serviceBackendConfig.resourceName = resources[i]; - break; + // Check if meta contains a resource without provider, so it is a migrated one. + if (analyticsMeta) { + for (const resourceName of Object.keys(analyticsMeta)) { + const resource = analyticsMeta[resourceName]; + + if (resource.service === 'Pinpoint' && !resource.providerPlugin) { + isMobileHubMigrated = true; + break; + } } } - } - if (pinpointApp) { - await notificationManager.pullAllChannels(context, pinpointApp); - pinpointNotificationsMeta = { - Name: pinpointApp.Name, - service: constants.PinpointName, - output: pinpointApp, - }; + if (!isMobileHubMigrated) { + pinpointApp = pinpointHelper.scanCategoryMetaForPinpoint(analyticsMeta); + } } - if (serviceBackendConfig) { - if (pinpointNotificationsMeta) { - pinpointNotificationsMeta.channels = serviceBackendConfig.channels; - } else { - pinpointNotificationsMeta = serviceBackendConfig; + // Special case, in case of mobile hub migrated projects there is no backend config, so skipping the next parts + // as all data is present in the service, no need for any updates. + + if (!isMobileHubMigrated) { + const backendConfigFilePath = context.amplify.pathManager.getBackendConfigFilePath(); + const backendConfig = context.amplify.readJsonFile(backendConfigFilePath); + if (backendConfig[constants.CategoryName]) { + const categoryConfig = backendConfig[constants.CategoryName]; + const resources = Object.keys(categoryConfig); + for (let i = 0; i < resources.length; i++) { + serviceBackendConfig = categoryConfig[resources[i]]; + if (serviceBackendConfig.service === constants.PinpointName) { + serviceBackendConfig.resourceName = resources[i]; + break; + } + } + } + + if (pinpointApp) { + await notificationManager.pullAllChannels(context, pinpointApp); + pinpointNotificationsMeta = { + Name: pinpointApp.Name, + service: constants.PinpointName, + output: pinpointApp, + }; } + + if (serviceBackendConfig) { + if (pinpointNotificationsMeta) { + pinpointNotificationsMeta.channels = serviceBackendConfig.channels; + } else { + pinpointNotificationsMeta = serviceBackendConfig; + } + } + + return pinpointNotificationsMeta; } - return pinpointNotificationsMeta; + return undefined; } async function deletePinpointAppForEnv(context, envName) { diff --git a/packages/amplify-category-notifications/lib/notifications-manager.js b/packages/amplify-category-notifications/lib/notifications-manager.js index cfec1d6619e..111defe103b 100644 --- a/packages/amplify-category-notifications/lib/notifications-manager.js +++ b/packages/amplify-category-notifications/lib/notifications-manager.js @@ -68,8 +68,16 @@ async function disableChannel(context, channelName) { async function configureChannel(context, channelName) { if (Object.keys(channelWorkers).indexOf(channelName) > -1) { context.exeInfo.pinpointClient = await pintpointHelper.getPinpointClient(context, 'update'); + + if (!context.exeInfo.serviceMeta.providerPlugin) { + context.print.error('No resources to update.'); + return false; + } + const channelWorker = require(path.join(__dirname, channelWorkers[channelName])); await channelWorker.configure(context); + + return true; } } diff --git a/packages/amplify-category-storage/index.js b/packages/amplify-category-storage/index.js index 4662ea82552..f25b96785a8 100644 --- a/packages/amplify-category-storage/index.js +++ b/packages/amplify-category-storage/index.js @@ -29,7 +29,7 @@ async function migrate(context) { const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); if (providerController) { migrateResourcePromises.push( - providerController.migrateResource(context, projectPath, amplifyMeta[category][resourceName].service, resourceName) + providerController.migrateResource(context, projectPath, amplifyMeta[category][resourceName].service, resourceName), ); } else { context.print.error(`Provider not configured for ${category}: ${resourceName}`); @@ -62,13 +62,13 @@ async function getPermissionPolicies(context, resourceOpsMapping) { ? resourceOpsMapping[resourceName].service : amplifyMeta[category][resourceName].service; - const providerController = require(`./provider-utils/${providerPlugin}/index`); - if (providerController) { + if (providerPlugin) { + const providerController = require(`./provider-utils/${providerPlugin}/index`); const { policy, attributes } = providerController.getPermissionPolicies( context, service, resourceName, - resourceOpsMapping[resourceName] + resourceOpsMapping[resourceName], ); permissionPolicies.push(policy); resourceAttributes.push({ resourceName, attributes, category }); diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js index 93761c2f6e6..88331bbeaa4 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js @@ -24,7 +24,7 @@ function updateWalkthrough(context, defaultValuesFilename, serviceMetadata) { const dynamoDbResources = {}; Object.keys(amplifyMeta[category]).forEach(resourceName => { - if (amplifyMeta[category][resourceName].service === serviceName) { + if (amplifyMeta[category][resourceName].service === serviceName && !!amplifyMeta[category][resourceName].providerPlugin) { dynamoDbResources[resourceName] = amplifyMeta[category][resourceName]; } }); diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js index 9aada0c3878..4e1e956197d 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js @@ -60,7 +60,7 @@ function updateWalkthrough(context, defaultValuesFilename, serviceMetada) { const storageResources = {}; Object.keys(amplifyMeta[category]).forEach(resourceName => { - if (amplifyMeta[category][resourceName].service === serviceName) { + if (amplifyMeta[category][resourceName].service === serviceName && !!amplifyMeta[category][resourceName].providerPlugin) { storageResources[resourceName] = amplifyMeta[category][resourceName]; } }); diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts b/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts index b4189ca3011..40d11c9998c 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts @@ -40,13 +40,15 @@ export async function removeResource( ) { const amplifyMeta = stateManager.getMeta(); - if (!amplifyMeta[category] || Object.keys(amplifyMeta[category]).length === 0) { + if (!amplifyMeta[category] || Object.keys(amplifyMeta[category]).filter(r => !!amplifyMeta[category][r].providerPlugin).length === 0) { context.print.error('No resources added for this category'); context.usageData.emitError(new ResourceDoesNotExistError()); process.exit(1); } - let enabledCategoryResources: { name; value } | { name; value }[] | string[] = Object.keys(amplifyMeta[category]); + let enabledCategoryResources: { name; value } | { name; value }[] | string[] = Object.keys(amplifyMeta[category]).filter( + r => !!amplifyMeta[category][r].providerPlugin, + ); if (resourceName) { if (!enabledCategoryResources.includes(resourceName)) { diff --git a/packages/amplify-cli/src/index.ts b/packages/amplify-cli/src/index.ts index 6cfa4356623..3f97a3c9287 100644 --- a/packages/amplify-cli/src/index.ts +++ b/packages/amplify-cli/src/index.ts @@ -1,6 +1,6 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { CLIContextEnvironmentProvider, FeatureFlags, pathManager, stateManager } from 'amplify-cli-core'; +import { $TSContext, CLIContextEnvironmentProvider, FeatureFlags, pathManager, stateManager } from 'amplify-cli-core'; import { Input } from './domain/input'; import { getPluginPlatform, scan } from './plugin-manager'; import { getCommandLineInput, verifyInput } from './input-manager'; @@ -16,6 +16,7 @@ import { notify } from './version-notifier'; // https://github.com/SBoudrias/Inquirer.js/issues/887 import { EventEmitter } from 'events'; import { rewireDeprecatedCommands } from './rewireDeprecatedCommands'; +import { ensureMobileHubCommandCompatibility } from './utils/mobilehub-support'; EventEmitter.defaultMaxListeners = 1000; // entry from commandline @@ -64,24 +65,40 @@ export async function run() { const projectPath = pathManager.findProjectRoot() ?? process.cwd(); const useNewDefaults = !stateManager.projectConfigExists(projectPath); + await FeatureFlags.initialize(contextEnvironmentProvider, useNewDefaults); await attachUsageData(context); + errorHandler = boundErrorHandler.bind(context); process.on('SIGINT', sigIntHandler.bind(context)); + await checkProjectConfigVersion(context); + context.usageData.emitInvoke(); + + // For mobile hub migrated project validate project and command to be executed + if (!ensureMobileHubCommandCompatibility((context as unknown) as $TSContext)) { + // Double casting until we have properly typed context + return 1; + } + await executeCommand(context); + const exitCode = process.exitCode || 0; + if (exitCode === 0) { context.usageData.emitSuccess(); } + persistContext(context); + // no command supplied defaults to help, give update notification at end of execution if (input.command === 'help') { // Checks for available update, defaults to a 1 day interval for notification notify({ defer: true, isGlobal: true }); } + return exitCode; } catch (e) { // ToDo: add logging to the core, and log execution errors using the unified core logging. diff --git a/packages/amplify-cli/src/migrate-project.ts b/packages/amplify-cli/src/migrate-project.ts index a896af91baf..04e7ef4e445 100644 --- a/packages/amplify-cli/src/migrate-project.ts +++ b/packages/amplify-cli/src/migrate-project.ts @@ -66,7 +66,7 @@ async function migrateFrom0To1(context: $TSContext, projectPath, projectConfig) try { amplifyDirPath = pathManager.getAmplifyDirPath(projectPath); backupAmplifyDirPath = backup(amplifyDirPath, projectPath); - context.migrationInfo = generateMigrationInfo(projectConfig, projectPath); + context.migrationInfo = { ...context.migrationInfo, ...generateMigrationInfo(projectConfig, projectPath) }; // Give each category a chance to migrate their respective files const categoryMigrationTasks: Function[] = []; diff --git a/packages/amplify-cli/src/utils/mobilehub-support.ts b/packages/amplify-cli/src/utils/mobilehub-support.ts new file mode 100644 index 00000000000..8d135f7048c --- /dev/null +++ b/packages/amplify-cli/src/utils/mobilehub-support.ts @@ -0,0 +1,48 @@ +import { $TSContext, stateManager } from 'amplify-cli-core'; + +export const ensureMobileHubCommandCompatibility = (context: $TSContext): boolean => { + checkIfMobileHubProject(context); + + // Only do further checks if it is mobile hub migrated project + if (context.migrationInfo.projectHasMobileHubResources !== true) { + return true; + } + + return isCommandSupported(context); +}; + +const checkIfMobileHubProject = (context: $TSContext): void => { + const meta = stateManager.getMeta(undefined, { throwIfNotExist: false }); + + if (!meta) { + return; + } + + let hasMigratedResources = false; + + Object.keys(meta).forEach(category => { + Object.keys(meta[category]).forEach(resourceName => { + const resource = meta[category][resourceName]; + + // Mobile hub migrated resources does not have an assigned provider + if (!resource.providerPlugin) { + hasMigratedResources = true; + } + }); + }); + + context.migrationInfo = { ...context.migrationInfo, projectHasMobileHubResources: hasMigratedResources }; +}; + +const isCommandSupported = (context: $TSContext): boolean => { + const { command, plugin } = context.input; + + // env commands are not supported for projects that having resources without provider assigned + if (command === 'env') { + context.print.error(`multi-environment support is not available for Amplify projects with Mobile Hub migrated resources.`); + + return false; + } + + return true; +}; diff --git a/packages/amplify-provider-awscloudformation/src/attach-backend.js b/packages/amplify-provider-awscloudformation/src/attach-backend.js index cf3822c65fe..64aa30b8158 100644 --- a/packages/amplify-provider-awscloudformation/src/attach-backend.js +++ b/packages/amplify-provider-awscloudformation/src/attach-backend.js @@ -103,11 +103,17 @@ async function getAmplifyApp(context, amplifyClient) { context.print.info(`Amplify AppID found: ${inputAmplifyAppId}. Amplify App name is: ${getAppResult.app.name}`); return getAppResult.app; } catch (e) { - context.print.error( - `Amplify AppID: ${inputAmplifyAppId} not found. Please ensure your local profile matches the AWS account or region in which the Amplify app exists.`, - ); - context.print.info(e); - throw e; + if (e.name && e.name === 'NotFoundException') { + const error = new Error(`${e.message} Check that the region of the Amplify App is matching the configured region.`); + error.stack = undefined; + throw error; + } else { + context.print.error( + `Amplify AppID: ${inputAmplifyAppId} not found. Please ensure your local profile matches the AWS account or region in which the Amplify app exists.`, + ); + context.print.info(e); + throw e; + } } } 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 3d6a0ac62e6..d9caf689d4a 100644 --- a/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js +++ b/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js @@ -10,6 +10,7 @@ const S3 = require('./aws-s3'); const providerName = require('../constants').ProviderName; const { formUserAgentParam } = require('./user-agent'); const configurationManager = require('../configuration-manager'); +const { stateManager } = require('amplify-cli-core'); const CFN_MAX_CONCURRENT_REQUEST = 5; const CFN_POLL_TIME = 5 * 1000; // 5 secs wait to check if new stacks are created by root stack @@ -277,81 +278,72 @@ class CloudFormation { }); } - updateamplifyMetaFileWithStackOutputs(parentStackName) { + async updateamplifyMetaFileWithStackOutputs(parentStackName) { const cfnParentStackParams = { StackName: parentStackName, }; const projectDetails = this.context.amplify.getProjectDetails(); const { amplifyMeta } = projectDetails; - const cfnModel = this.cfn; - return cfnModel - .describeStackResources(cfnParentStackParams) - .promise() - .then(result => { - let resources = result.StackResources; - resources = resources.filter( - resource => - ![ - 'DeploymentBucket', - 'AuthRole', - 'UnauthRole', - 'UpdateRolesWithIDPFunction', - 'UpdateRolesWithIDPFunctionOutputs', - 'UpdateRolesWithIDPFunctionRole', - ].includes(resource.LogicalResourceId), - ); - - const promises = []; - - for (let i = 0; i < resources.length; i += 1) { - const cfnNestedStackParams = { - StackName: resources[i].PhysicalResourceId, - }; - promises.push(this.describeStack(cfnNestedStackParams)); - } + const result = await this.cfn.describeStackResources(cfnParentStackParams).promise(); + const resources = result.StackResources.filter( + resource => + ![ + 'DeploymentBucket', + 'AuthRole', + 'UnauthRole', + 'UpdateRolesWithIDPFunction', + 'UpdateRolesWithIDPFunctionOutputs', + 'UpdateRolesWithIDPFunctionRole', + ].includes(resource.LogicalResourceId), + ); - return Promise.all(promises).then(stackResult => { - Object.keys(amplifyMeta).forEach(category => { - Object.keys(amplifyMeta[category]).forEach(resource => { - const logicalResourceId = category + resource; - const index = resources.findIndex(resourceItem => resourceItem.LogicalResourceId === logicalResourceId); - if (index !== -1) { - const formattedOutputs = formatOutputs(stackResult[index].Stacks[0].Outputs); - - const updatedMeta = this.context.amplify.updateamplifyMetaAfterResourceUpdate( - category, - resource, - 'output', - formattedOutputs, - ); - - // Check to see if this is an AppSync resource and if we've to remove the GraphQLAPIKeyOutput from meta or not - if (amplifyMeta[category][resource]) { - const resourceObject = amplifyMeta[category][resource]; - - if ( - resourceObject.service === 'AppSync' && - resourceObject.output && - resourceObject.output.GraphQLAPIKeyOutput && - !formattedOutputs.GraphQLAPIKeyOutput - ) { - const updatedResourceObject = updatedMeta[category][resource]; - - if (updatedResourceObject.output.GraphQLAPIKeyOutput) { - delete updatedResourceObject.output.GraphQLAPIKeyOutput; - } - } + if (resources.length > 0) { + const promises = []; + + for (let i = 0; i < resources.length; i++) { + const cfnNestedStackParams = { + StackName: resources[i].PhysicalResourceId, + }; + + promises.push(this.describeStack(cfnNestedStackParams)); + } + + const stackResult = await Promise.all(promises); + + Object.keys(amplifyMeta).forEach(category => { + Object.keys(amplifyMeta[category]).forEach(resource => { + const logicalResourceId = category + resource; + const index = resources.findIndex(resourceItem => resourceItem.LogicalResourceId === logicalResourceId); - const amplifyMetaFilePath = this.context.amplify.pathManager.getAmplifyMetaFilePath(); - const jsonString = JSON.stringify(updatedMeta, null, 4); - fs.writeFileSync(amplifyMetaFilePath, jsonString, 'utf8'); + if (index !== -1) { + const formattedOutputs = formatOutputs(stackResult[index].Stacks[0].Outputs); + + const updatedMeta = this.context.amplify.updateamplifyMetaAfterResourceUpdate(category, resource, 'output', formattedOutputs); + + // Check to see if this is an AppSync resource and if we've to remove the GraphQLAPIKeyOutput from meta or not + if (amplifyMeta[category][resource]) { + const resourceObject = amplifyMeta[category][resource]; + + if ( + resourceObject.service === 'AppSync' && + resourceObject.output && + resourceObject.output.GraphQLAPIKeyOutput && + !formattedOutputs.GraphQLAPIKeyOutput + ) { + const updatedResourceObject = updatedMeta[category][resource]; + + if (updatedResourceObject.output.GraphQLAPIKeyOutput) { + delete updatedResourceObject.output.GraphQLAPIKeyOutput; } } - }); - }); + + stateManager.setMeta(undefined, updatedMeta); + } + } }); }); + } } listExports(nextToken = null) { diff --git a/packages/amplify-provider-awscloudformation/src/initialize-env.js b/packages/amplify-provider-awscloudformation/src/initialize-env.js index 7804f3f75e7..f469dd97fff 100644 --- a/packages/amplify-provider-awscloudformation/src/initialize-env.js +++ b/packages/amplify-provider-awscloudformation/src/initialize-env.js @@ -1,13 +1,14 @@ const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); -const { PathConstants, stateManager } = require('amplify-cli-core'); +const _ = require('lodash'); +const { JSONUtilities, PathConstants, stateManager } = require('amplify-cli-core'); const Cloudformation = require('./aws-utils/aws-cfn'); const S3 = require('./aws-utils/aws-s3'); const { downloadZip, extractZip } = require('./zip-util'); const { S3BackendZipFileName } = require('./constants'); -function run(context, providerMetadata) { +async function run(context, providerMetadata) { if (context.exeInfo && context.exeInfo.isNewEnv) { return context; } @@ -17,62 +18,93 @@ function run(context, providerMetadata) { const currentCloudBackendDir = context.amplify.pathManager.getCurrentCloudBackendDirPath(); const backendDir = context.amplify.pathManager.getBackendDirPath(); - return new S3(context) - .then(s3 => - downloadZip(s3, tempDir, S3BackendZipFileName).then(file => - extractZip(tempDir, file).then(unzippeddir => { - fs.removeSync(currentCloudBackendDir); - - // Move out cli.*json if exists in the temp directory into the amplify directory before copying backand and - // current cloud backend directories. - const cliJSONFiles = glob.sync(PathConstants.CLIJSONFileNameGlob, { - cwd: unzippeddir, - absolute: true, - }); - - if (context.exeInfo.restoreBackend) { - // If backend must be restored then copy out the config files and overwrite existing ones. - for (const cliJSONFilePath of cliJSONFiles) { - const targetPath = path.join(amplifyDir, path.basename(cliJSONFilePath)); - - fs.moveSync(cliJSONFilePath, targetPath, { overwrite: true }); - } - } else { - // If backend is not being restored, just delete the config files in the current cloud backend if present - for (const cliJSONFilePath of cliJSONFiles) { - fs.removeSync(cliJSONFilePath); - } - } - - fs.copySync(unzippeddir, currentCloudBackendDir); - - if (context.exeInfo.restoreBackend) { - fs.removeSync(backendDir); - fs.copySync(unzippeddir, backendDir); - } - fs.removeSync(tempDir); - }), - ), - ) - .then(() => new Cloudformation(context)) - .then(cfnItem => cfnItem.updateamplifyMetaFileWithStackOutputs(providerMetadata.StackName)) - .then(() => { - // Copy provider metadata from current-cloud-backend/amplify-meta to backend/ampliy-meta - const currentAmplifyMeta = stateManager.getCurrentMeta(); - const amplifyMeta = stateManager.getMeta(); - - // Copy providerMetadata for each resource - from what is there in the cloud - - Object.keys(amplifyMeta).forEach(category => { - Object.keys(amplifyMeta[category]).forEach(resource => { - if (currentAmplifyMeta[category] && currentAmplifyMeta[category][resource]) { - amplifyMeta[category][resource].providerMetadata = currentAmplifyMeta[category][resource].providerMetadata; - } - }); - }); - - stateManager.setMeta(undefined, amplifyMeta); + const s3 = await new S3(context); + const file = await downloadZip(s3, tempDir, S3BackendZipFileName); + const unzippeddir = await extractZip(tempDir, file); + + fs.removeSync(currentCloudBackendDir); + + // Move out cli.*json if exists in the temp directory into the amplify directory before copying backand and + // current cloud backend directories. + const cliJSONFiles = glob.sync(PathConstants.CLIJSONFileNameGlob, { + cwd: unzippeddir, + absolute: true, + }); + + if (context.exeInfo.restoreBackend) { + // If backend must be restored then copy out the config files and overwrite existing ones. + for (const cliJSONFilePath of cliJSONFiles) { + const targetPath = path.join(amplifyDir, path.basename(cliJSONFilePath)); + + fs.moveSync(cliJSONFilePath, targetPath, { overwrite: true }); + } + } else { + // If backend is not being restored, just delete the config files in the current cloud backend if present + for (const cliJSONFilePath of cliJSONFiles) { + fs.removeSync(cliJSONFilePath); + } + } + + fs.copySync(unzippeddir, currentCloudBackendDir); + + if (context.exeInfo.restoreBackend) { + fs.removeSync(backendDir); + fs.copySync(unzippeddir, backendDir); + } + + fs.removeSync(tempDir); + + const cfnItem = await new Cloudformation(context); + + await cfnItem.updateamplifyMetaFileWithStackOutputs(providerMetadata.StackName); + + // Copy provider metadata from current-cloud-backend/amplify-meta to backend/ampliy-meta + const currentAmplifyMeta = stateManager.getCurrentMeta(); + const amplifyMeta = stateManager.getMeta(); + + // Copy providerMetadata for each resource - from what is there in the cloud + + Object.keys(amplifyMeta).forEach(category => { + Object.keys(amplifyMeta[category]).forEach(resource => { + if (currentAmplifyMeta[category] && currentAmplifyMeta[category][resource]) { + amplifyMeta[category][resource].providerMetadata = currentAmplifyMeta[category][resource].providerMetadata; + } }); + }); + + // + // Download the meta file from the bucket and see if it has migrated resources (no provider field) + // copy those over to the reconstructed meta file. + // + + let hasMigratedResources = false; + const s3AmplifyMeta = JSONUtilities.parse( + ( + await s3.getFile({ + Key: PathConstants.AmplifyMetaFileName, + }) + ).toString(), + ); + + Object.keys(s3AmplifyMeta).forEach(category => { + Object.keys(s3AmplifyMeta[category]).forEach(resourceName => { + const resource = s3AmplifyMeta[category][resourceName]; + + // Mobile hub migrated resources does not have an assigned provider + if (!resource.providerPlugin) { + _.set(amplifyMeta, [category, resourceName], resource); + hasMigratedResources = true; + } + }); + }); + + stateManager.setMeta(undefined, amplifyMeta); + + // If the project has any mobile hub migrated projects then to show no diff between + // cloud and local env we have to copy the new meta to current cloud backend as well. + if (hasMigratedResources) { + stateManager.setCurrentMeta(undefined, amplifyMeta); + } } module.exports = { From 806df65aa87a87c9133502c42d8da800cc1da36e Mon Sep 17 00:00:00 2001 From: Attila Hajdrik Date: Fri, 25 Sep 2020 10:53:41 -0700 Subject: [PATCH 2/5] fix: sigint test failure --- packages/amplify-cli/src/__tests__/test-aborting.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/amplify-cli/src/__tests__/test-aborting.test.ts b/packages/amplify-cli/src/__tests__/test-aborting.test.ts index 53b2d28ff2d..db156d684ce 100644 --- a/packages/amplify-cli/src/__tests__/test-aborting.test.ts +++ b/packages/amplify-cli/src/__tests__/test-aborting.test.ts @@ -19,6 +19,7 @@ describe('test SIGINT with execute', () => { findProjectRoot: jest.fn(), }, stateManager: { + getMeta: jest.fn(), projectConfigExists: jest.fn(), }, FeatureFlags: { @@ -48,6 +49,9 @@ describe('test SIGINT with execute', () => { emitSuccess: jest.fn(), init: jest.fn(), }; + mockContext.migrationInfo = { + projectHasMobileHubResources: false, + }; mockContext.amplify = jest.genMockFromModule('../domain/amplify-toolkit'); jest.setMock('../context-manager', { constructContext: jest.fn().mockReturnValue(mockContext), From a0cfbb886a8b1a7efa3043d557bc332b307d0c74 Mon Sep 17 00:00:00 2001 From: Attila Hajdrik Date: Fri, 25 Sep 2020 11:17:00 -0700 Subject: [PATCH 3/5] chore: fix lgtm warning --- packages/amplify-cli/src/utils/mobilehub-support.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify-cli/src/utils/mobilehub-support.ts b/packages/amplify-cli/src/utils/mobilehub-support.ts index 8d135f7048c..dec21b95626 100644 --- a/packages/amplify-cli/src/utils/mobilehub-support.ts +++ b/packages/amplify-cli/src/utils/mobilehub-support.ts @@ -35,7 +35,7 @@ const checkIfMobileHubProject = (context: $TSContext): void => { }; const isCommandSupported = (context: $TSContext): boolean => { - const { command, plugin } = context.input; + const { command } = context.input; // env commands are not supported for projects that having resources without provider assigned if (command === 'env') { From 6203a81dcceb314c3e656f25494cf0a500645ccb Mon Sep 17 00:00:00 2001 From: Attila Hajdrik Date: Wed, 30 Sep 2020 11:07:22 -0700 Subject: [PATCH 4/5] fix: add 'add notification' and 'update auth' protection --- .../src/commands/auth/update.js | 11 +++++++++++ .../commands/notifications/add.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/amplify-category-auth/src/commands/auth/update.js b/packages/amplify-category-auth/src/commands/auth/update.js index 312aee6f6cd..cf2a850f3a7 100644 --- a/packages/amplify-category-auth/src/commands/auth/update.js +++ b/packages/amplify-category-auth/src/commands/auth/update.js @@ -18,6 +18,17 @@ module.exports = { if (!Object.keys(existingAuth).length > 0) { return context.print.warning('Auth has not yet been added to this project.'); + } else { + const services = Object.keys(existingAuth); + + for (let i = 0; i < services.length; i++) { + const serviceMeta = existingAuth[services[i]]; + + if (!serviceMeta.providerPlugin) { + context.print.error('Auth is migrated from Mobile Hub and cannot be updated with Amplify CLI.'); + return context; + } + } } context.print.info('Please note that certain attributes may not be overwritten if you choose to use defaults settings.'); diff --git a/packages/amplify-category-notifications/commands/notifications/add.js b/packages/amplify-category-notifications/commands/notifications/add.js index c890a169fb6..a841a82a191 100644 --- a/packages/amplify-category-notifications/commands/notifications/add.js +++ b/packages/amplify-category-notifications/commands/notifications/add.js @@ -1,6 +1,7 @@ const inquirer = require('inquirer'); const pinpointHelper = require('../../lib/pinpoint-helper'); const notificationManager = require('../../lib/notifications-manager'); +const constants = require('../../lib/constants'); const multiEnvManager = require('../../lib/multi-env-manager'); module.exports = { @@ -8,6 +9,20 @@ module.exports = { alias: 'enable', run: async context => { context.exeInfo = context.amplify.getProjectDetails(); + + const categoryMeta = context.exeInfo.amplifyMeta[constants.CategoryName]; + if (categoryMeta) { + const services = Object.keys(categoryMeta); + for (let i = 0; i < services.length; i++) { + const serviceMeta = categoryMeta[services[i]]; + + if (!serviceMeta.providerPlugin) { + context.print.error('Notifications is migrated from Mobile Hub and channels cannot be added with Amplify CLI.'); + return context; + } + } + } + const availableChannels = notificationManager.getAvailableChannels(context); const disabledChannels = notificationManager.getDisabledChannels(context); From cf0004fb306f8dce2dcc544ce11a00af5bbdbe33 Mon Sep 17 00:00:00 2001 From: Attila Hajdrik Date: Wed, 30 Sep 2020 11:37:03 -0700 Subject: [PATCH 5/5] chore: fix update unit test --- packages/amplify-category-auth/src/commands/auth/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify-category-auth/src/commands/auth/update.js b/packages/amplify-category-auth/src/commands/auth/update.js index cf2a850f3a7..a914908a2af 100644 --- a/packages/amplify-category-auth/src/commands/auth/update.js +++ b/packages/amplify-category-auth/src/commands/auth/update.js @@ -24,7 +24,7 @@ module.exports = { for (let i = 0; i < services.length; i++) { const serviceMeta = existingAuth[services[i]]; - if (!serviceMeta.providerPlugin) { + if (serviceMeta.service === 'Cognito' && !serviceMeta.providerPlugin) { context.print.error('Auth is migrated from Mobile Hub and cannot be updated with Amplify CLI.'); return context; }