Skip to content

Commit

Permalink
feat: parameter store integration for env parameters (#12016)
Browse files Browse the repository at this point in the history
Co-authored-by: Zachary Goldberg <[email protected]>
Co-authored-by: Spencer Stolworthy <[email protected]>
  • Loading branch information
3 people authored Feb 15, 2023
1 parent 7f268f7 commit 3112646
Show file tree
Hide file tree
Showing 35 changed files with 1,199 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ pathManagerMock.getBackendDirPath.mockReturnValue(path.join('test', 'path'));

getAppIdMock.mockReturnValue('testAppId');

SSMClientWrapperMock.getInstance.mockResolvedValue(({
SSMClientWrapperMock.getInstance.mockResolvedValue({
deleteSecret: jest.fn(),
setSecret: jest.fn(),
} as unknown) as SSMClientWrapper);
} as unknown as SSMClientWrapper);

describe('syncSecretDeltas', () => {
const contextStub = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { $TSAny, $TSContext, spinner } from 'amplify-cli-core';
import aws from 'aws-sdk';
import { $TSContext } from 'amplify-cli-core';
import type { SSM } from 'aws-sdk';

/**
* Wrapper around SSM SDK calls
*/
// eslint-disable-next-line
export class SSMClientWrapper {
private static instance: SSMClientWrapper;

Expand All @@ -15,12 +14,12 @@ export class SSMClientWrapper {
return SSMClientWrapper.instance;
};

private constructor(private readonly ssmClient: aws.SSM) {}
private constructor(private readonly ssmClient: SSM) {}

/**
* Returns a list of secret name value pairs
*/
getSecrets = async (secretNames: string[]): Promise<$TSAny> => {
getSecrets = async (secretNames: string[]): Promise<{ secretName?: string, secretValue?: string }[] | undefined> => {
if (!secretNames || secretNames.length === 0) {
return [];
}
Expand All @@ -31,7 +30,7 @@ export class SSMClientWrapper {
})
.promise();

return result.Parameters.map(({ Name, Value }) => ({ secretName: Name, secretValue: Value }));
return result?.Parameters?.map(({ Name, Value }) => ({ secretName: Name, secretValue: Value }));
};

/**
Expand All @@ -41,8 +40,7 @@ export class SSMClientWrapper {
let NextToken;
const accumulator: string[] = [];
do {
// eslint-disable-next-line
const result = await this.ssmClient
const result: SSM.GetParametersByPathResult = await this.ssmClient
.getParametersByPath({
Path: secretPath,
MaxResults: 10,
Expand All @@ -56,7 +54,11 @@ export class SSMClientWrapper {
NextToken,
})
.promise();
accumulator.push(...result.Parameters.map(param => param.Name));

if (Array.isArray(result?.Parameters)) {
accumulator.push(...result.Parameters.filter(param => param?.Name !== undefined).map(param => param.Name));
}

NextToken = result.NextToken;
} while (NextToken);
return accumulator;
Expand Down Expand Up @@ -108,17 +110,14 @@ export class SSMClientWrapper {
};
}

const getSSMClient = async (context: $TSContext): Promise<aws.SSM> => {
try {
spinner.start();
spinner.text = 'Building and packaging resources';

const { client } = await context.amplify.invokePluginMethod<{ client: aws.SSM }>(context, 'awscloudformation', undefined, 'getConfiguredSSMClient', [
context,
]);
const getSSMClient = async (context: $TSContext): Promise<SSM> => {
const { client } = await context.amplify.invokePluginMethod<{ client: SSM }>(
context,
'awscloudformation',
undefined,
'getConfiguredSSMClient',
[context],
);

return client;
} finally {
spinner.stop();
}
return client;
};
5 changes: 3 additions & 2 deletions packages/amplify-cli-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class AmplifyError extends AmplifyException {
}

// @public (undocumented)
export type AmplifyErrorType = 'AmplifyStudioError' | 'AmplifyStudioLoginError' | 'AmplifyStudioNotEnabledError' | 'ApiCategorySchemaNotFoundError' | 'AuthImportError' | 'BackendConfigValidationError' | 'BucketAlreadyExistsError' | 'BucketNotFoundError' | 'CategoryNotEnabledError' | 'CloudFormationTemplateError' | 'CommandNotSupportedError' | 'ConfigurationError' | 'DeploymentError' | 'DeploymentInProgressError' | 'DirectoryError' | 'DirectoryAlreadyExistsError' | 'DuplicateLogicalIdError' | 'EnvironmentConfigurationError' | 'EnvironmentNameError' | 'EnvironmentNotInitializedError' | 'FeatureFlagsValidationError' | 'FrameworkNotSupportedError' | 'FunctionTooLargeError' | 'InputValidationError' | 'InvalidAmplifyAppIdError' | 'InvalidCustomResourceError' | 'InvalidOverrideError' | 'InvalidStackError' | 'IterativeRollbackError' | 'LambdaLayerDeleteError' | 'MigrationError' | 'MissingAmplifyMetaFileError' | 'MissingOverridesInstallationRequirementsError' | 'ModelgenError' | 'NestedProjectInitError' | 'NoUpdateBackendError' | 'NotImplementedError' | 'OpenSslCertificateError' | 'ParameterNotFoundError' | 'PermissionsError' | 'PluginMethodNotFoundError' | 'PluginNotFoundError' | 'PluginPolicyAddError' | 'ProfileConfigurationError' | 'ProjectAppIdResolveError' | 'ProjectInitError' | 'ProjectNotFoundError' | 'ProjectNotInitializedError' | 'PushResourcesError' | 'RegionNotAvailableError' | 'RemoveNotificationAppError' | 'ResourceAlreadyExistsError' | 'ResourceInUseError' | 'ResourceNotReadyError' | 'StackNotFoundError' | 'StackStateError' | 'UnsupportedLockFileTypeError' | 'UserInputError' | 'MockProcessError' | 'SearchableMockUnsupportedPlatformError' | 'SearchableMockUnavailablePortError' | 'SearchableMockProcessError';
export type AmplifyErrorType = 'AmplifyStudioError' | 'AmplifyStudioLoginError' | 'AmplifyStudioNotEnabledError' | 'ApiCategorySchemaNotFoundError' | 'AuthImportError' | 'BackendConfigValidationError' | 'BucketAlreadyExistsError' | 'BucketNotFoundError' | 'CategoryNotEnabledError' | 'CloudFormationTemplateError' | 'CommandNotSupportedError' | 'ConfigurationError' | 'DeploymentError' | 'DeploymentInProgressError' | 'DirectoryError' | 'DirectoryAlreadyExistsError' | 'DuplicateLogicalIdError' | 'EnvironmentConfigurationError' | 'EnvironmentNameError' | 'EnvironmentNotInitializedError' | 'FeatureFlagsValidationError' | 'FrameworkNotSupportedError' | 'FunctionTooLargeError' | 'InputValidationError' | 'InvalidAmplifyAppIdError' | 'InvalidCustomResourceError' | 'InvalidOverrideError' | 'InvalidStackError' | 'IterativeRollbackError' | 'LambdaLayerDeleteError' | 'MigrationError' | 'MissingAmplifyMetaFileError' | 'MissingExpectedParameterError' | 'MissingOverridesInstallationRequirementsError' | 'ModelgenError' | 'NestedProjectInitError' | 'NoUpdateBackendError' | 'NotImplementedError' | 'OpenSslCertificateError' | 'ParameterNotFoundError' | 'PermissionsError' | 'PluginMethodNotFoundError' | 'PluginNotFoundError' | 'PluginPolicyAddError' | 'ProfileConfigurationError' | 'ProjectAppIdResolveError' | 'ProjectInitError' | 'ProjectNotFoundError' | 'ProjectNotInitializedError' | 'PushResourcesError' | 'RegionNotAvailableError' | 'RemoveNotificationAppError' | 'ResourceAlreadyExistsError' | 'ResourceInUseError' | 'ResourceNotReadyError' | 'StackNotFoundError' | 'StackStateError' | 'UnsupportedLockFileTypeError' | 'UserInputError' | 'MockProcessError' | 'SearchableMockUnsupportedPlatformError' | 'SearchableMockUnavailablePortError' | 'SearchableMockProcessError';

// @public (undocumented)
export enum AmplifyEvent {
Expand Down Expand Up @@ -230,7 +230,7 @@ export class AmplifyFault extends AmplifyException {
}

// @public (undocumented)
export type AmplifyFaultType = 'AnalyticsCategoryFault' | 'AmplifyBackupFault' | 'BackendPullFault' | 'ConfigurationFault' | 'BackendDeleteFault' | 'ConfigurationFault' | 'DeploymentFault' | 'LockFileNotFoundFault' | 'LockFileParsingFault' | 'NotificationsChannelAPNSFault' | 'NotificationsChannelEmailFault' | 'NotificationsChannelFCMFault' | 'NotificationsChannelSmsFault' | 'NotificationsChannelInAppMessagingFault' | 'NotImplementedFault' | 'ProjectDeleteFault' | 'ProjectInitFault' | 'PluginNotLoadedFault' | 'PushResourcesFault' | 'PullBackendFault' | 'ResourceExportFault' | 'ResourceNotFoundFault' | 'ResourceNotReadyFault' | 'ResourceRemoveFault' | 'RootStackNotFoundFault' | 'ServiceCallFault' | 'SnsSandboxModeCheckFault' | 'TimeoutFault' | 'TriggerUploadFault' | 'UnexpectedS3Fault' | 'UnknownFault' | 'UnknownNodeJSFault' | 'MockProcessFault' | 'AuthCategoryFault' | 'ZipExtractFault';
export type AmplifyFaultType = 'AnalyticsCategoryFault' | 'AmplifyBackupFault' | 'BackendPullFault' | 'ConfigurationFault' | 'BackendDeleteFault' | 'ConfigurationFault' | 'DeploymentFault' | 'LockFileNotFoundFault' | 'LockFileParsingFault' | 'NotificationsChannelAPNSFault' | 'NotificationsChannelEmailFault' | 'NotificationsChannelFCMFault' | 'NotificationsChannelSmsFault' | 'NotificationsChannelInAppMessagingFault' | 'NotImplementedFault' | 'ParameterDownloadFault' | 'ParameterUploadFault' | 'ProjectDeleteFault' | 'ParametersDeleteFault' | 'ProjectInitFault' | 'PluginNotLoadedFault' | 'PushResourcesFault' | 'PullBackendFault' | 'ResourceExportFault' | 'ResourceNotFoundFault' | 'ResourceNotReadyFault' | 'ResourceRemoveFault' | 'RootStackNotFoundFault' | 'ServiceCallFault' | 'SnsSandboxModeCheckFault' | 'TimeoutFault' | 'TriggerUploadFault' | 'UnexpectedS3Fault' | 'UnknownFault' | 'UnknownNodeJSFault' | 'MockProcessFault' | 'AuthCategoryFault' | 'ZipExtractFault';

// @public (undocumented)
export enum AmplifyFrontend {
Expand Down Expand Up @@ -559,6 +559,7 @@ export const constants: {
CODEGEN: string;
AMPLIFY: string;
DOT_AMPLIFY_DIR_NAME: string;
DEFAULT_PROVIDER: string;
AMPLIFY_PREFIX: string;
LOCAL_NODE_MODULES: string;
PARENT_DIRECTORY: string;
Expand Down
1 change: 1 addition & 0 deletions packages/amplify-cli-core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const constants = {
CODEGEN: 'codegen',
AMPLIFY: 'amplify',
DOT_AMPLIFY_DIR_NAME: '.amplify',
DEFAULT_PROVIDER: 'awscloudformation',
AMPLIFY_PREFIX: 'amplify-',
LOCAL_NODE_MODULES: 'cli-local-node-modules',
PARENT_DIRECTORY: 'cli-parent-directory',
Expand Down
27 changes: 17 additions & 10 deletions packages/amplify-cli-core/src/errors/amplify-exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ export abstract class AmplifyException extends Error {
}

toObject = (): object => {
const {
name: errorName, message: errorMessage, details: errorDetails, resolution, link, stack,
} = this;
const { name: errorName, message: errorMessage, details: errorDetails, resolution, link, stack } = this;

return {
errorName, errorMessage, errorDetails, resolution, link, ...(process.argv.includes('--debug') ? { stack } : {}),
errorName,
errorMessage,
errorDetails,
resolution,
link,
...(process.argv.includes('--debug') ? { stack } : {}),
};
}
};
}

/**
Expand All @@ -66,13 +69,13 @@ export type AmplifyExceptionClassification = 'FAULT' | 'ERROR';
* Amplify Error options object
*/
export type AmplifyExceptionOptions = {
message: string,
details?: string,
resolution?: string,
link?: string,
message: string;
details?: string;
resolution?: string;
link?: string;

// CloudFormation or NodeJS error codes
code?: string,
code?: string;
};

/**
Expand Down Expand Up @@ -123,6 +126,7 @@ export type AmplifyErrorType =
| 'LambdaLayerDeleteError'
| 'MigrationError'
| 'MissingAmplifyMetaFileError'
| 'MissingExpectedParameterError'
| 'MissingOverridesInstallationRequirementsError'
| 'ModelgenError'
| 'NestedProjectInitError'
Expand Down Expand Up @@ -173,7 +177,10 @@ export type AmplifyFaultType =
| 'NotificationsChannelSmsFault'
| 'NotificationsChannelInAppMessagingFault'
| 'NotImplementedFault'
| 'ParameterDownloadFault'
| 'ParameterUploadFault'
| 'ProjectDeleteFault'
| 'ParametersDeleteFault'
| 'ProjectInitFault'
| 'PluginNotLoadedFault'
| 'PushResourcesFault'
Expand Down
15 changes: 14 additions & 1 deletion packages/amplify-cli-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,21 @@ interface AmplifyToolkit {
updateBackendConfigAfterResourceAdd: (category: string, resourceName: string, resourceData: $TSObject) => void;
updateBackendConfigAfterResourceUpdate: (category: string, resourceName: string, attribute: string, value: $TSAny) => void;
updateBackendConfigAfterResourceRemove: (category: string, resourceName: string) => void;
/**
* use EnvironmentParameterManager from the amplify-environment-parameters package
* @deprecated
*/
loadEnvResourceParameters: (context: $TSContext, category: string, resourceName: string) => $TSAny;
saveEnvResourceParameters: (context: $TSContext, category: string, resourceName: string, envSpecificParams?: $TSObject) => void;
/**
* use EnvironmentParameterManager from the amplify-environment-parameters package
* @deprecated
*/
saveEnvResourceParameters: (
context: $TSContext | undefined,
category: string,
resourceName: string,
envSpecificParams?: $TSObject,
) => void;
removeResourceParameters: (context: $TSContext, category: string, resource: string) => void;
triggerFlow: (...args: unknown[]) => $TSAny;
addTrigger: () => $TSAny;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('deleteProject', () => {
amplify: {
getEnvDetails: () => [],
getProjectConfig: () => ({ frontend: 'test' }),
invokePluginMethod: async () => {},
},
filesystem: {
remove: jest.fn(),
Expand Down
6 changes: 3 additions & 3 deletions packages/amplify-cli/src/commands/env/remove.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import ora from 'ora';
import {
FeatureFlags, stateManager, $TSContext, AmplifyError,
} from 'amplify-cli-core';
import { FeatureFlags, stateManager, $TSContext, AmplifyError } from 'amplify-cli-core';
import { printer } from 'amplify-prompts';
import { getConfirmation } from '../../extensions/amplify-helpers/delete-project';
import { removeEnvFromCloud } from '../../extensions/amplify-helpers/remove-env-from-cloud';
import { invokeDeleteEnvParamsFromService } from '../../extensions/amplify-helpers/invoke-delete-env-params';

/**
* Entry point for env subcommand
Expand Down Expand Up @@ -41,6 +40,7 @@ export const run = async (context: $TSContext): Promise<void> => {
spinner.start();
try {
await removeEnvFromCloud(context, envName, confirmation.deleteS3);
await invokeDeleteEnvParamsFromService(context, envName);
} catch (ex) {
// safely exit spinner, then allow the exception to propagate up
spinner.fail(`remove env failed: ${ex.message}`);
Expand Down
9 changes: 5 additions & 4 deletions packages/amplify-cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const constructExeInfo = (context: $TSContext): void => {
};

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const runStrategy = (quickstart: boolean) => (quickstart
? [preInitSetup, analyzeProjectHeadless, scaffoldProjectHeadless, onHeadlessSuccess]
: [preInitSetup, analyzeProject, initFrontend, initProviders, onSuccess, postInitSetup]);
const runStrategy = (quickstart: boolean) =>
quickstart
? [preInitSetup, analyzeProjectHeadless, scaffoldProjectHeadless, onHeadlessSuccess]
: [preInitSetup, analyzeProject, initFrontend, initProviders, onSuccess, postInitSetup];

/**
* entry point for the init command
Expand All @@ -35,6 +36,6 @@ export const run = async (context: $TSContext): Promise<void> => {
}

if (context.exeInfo.sourceEnvName && context.exeInfo.localEnvInfo.envName) {
await raisePostEnvAddEvent(context as unknown as Context, context.exeInfo.sourceEnvName, context.exeInfo.localEnvInfo.envName);
await raisePostEnvAddEvent((context as unknown) as Context, context.exeInfo.sourceEnvName, context.exeInfo.localEnvInfo.envName);
}
};
5 changes: 3 additions & 2 deletions packages/amplify-cli/src/execution-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import sequential from 'promise-sequential';
import { prompter } from 'amplify-prompts';
import { twoStringSetsAreEqual, twoStringSetsAreDisjoint } from './utils/set-ops';
import { Context } from './domain/context';
Expand Down Expand Up @@ -335,7 +334,9 @@ export const raiseEvent = async <T extends AmplifyEvent>(context: Context, args:
};
return eventHandler;
});
await sequential(eventHandlers);
for (const eventHandler of eventHandlers) {
await eventHandler();
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getFrontendPlugins } from './get-frontend-plugins';
import { getPluginInstance } from './get-plugin-instance';
import { getAmplifyAppId } from './get-amplify-appId';
import { getAmplifyDirPath } from './path-manager';
import { invokeDeleteEnvParamsFromService } from '../../extensions/amplify-helpers/invoke-delete-env-params';

/**
* Deletes the amplify project from the cloud and local machine
Expand Down Expand Up @@ -41,6 +42,10 @@ export const deleteProject = async (context: $TSContext): Promise<void> => {
printer.warn('Amplify App cannot be deleted, other environments still linked to Application');
}
}

// delete env parameters from service for each env
await Promise.all(envNames.map(envName => invokeDeleteEnvParamsFromService(context, envName)));

spinner.succeed('Project deleted in the cloud.');
} catch (ex) {
if ('name' in ex && ex.name === 'BucketNotFoundError') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { $TSContext, constants } from 'amplify-cli-core';

export const invokeDeleteEnvParamsFromService = async (context: $TSContext, envName: string): Promise<void> => {
const CloudFormationProviderName = constants.DEFAULT_PROVIDER;
await context.amplify.invokePluginMethod(context, CloudFormationProviderName, undefined, 'deleteEnvironmentParametersFromService', [
context,
envName,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
stateManager,
} from 'amplify-cli-core';
import { generateDependentResourcesType } from '@aws-amplify/amplify-category-custom';
import { ensureEnvParamManager, IEnvironmentParameterManager } from '@aws-amplify/amplify-environment-parameters';
import { printer, prompter } from 'amplify-prompts';
import { getResources } from '../../commands/build';
import { initializeEnv } from '../../initialize-env';
Expand Down Expand Up @@ -90,6 +91,50 @@ export const pushResources = async (
return false;
}

// Verify any environment parameters before push operation
const envParamManager = (await ensureEnvParamManager()).instance;

const promptMissingParameter = async (
categoryName: string,
resourceName: string,
parameterName: string,
envParamManager: IEnvironmentParameterManager,
): Promise<void> => {
printer.warn(`Could not find value for parameter ${parameterName}`);
const value = await prompter.input(`Enter a value for ${parameterName} for the ${categoryName} resource: ${resourceName}`);
const resourceParamManager = envParamManager.getResourceParamManager(categoryName, resourceName);
resourceParamManager.setParam(parameterName, value);
};

const parametersToCheck = resourcesToBuild
.filter(({ category: c, resourceName: r }) => {
// Filter based on optional parameters
if (category) {
if (c !== category) {
return false;
}
}
if (resourceName) {
if (r !== resourceName) {
return false;
}
}
return true;
});


if (context?.exeInfo?.inputParams?.yes || context?.exeInfo?.inputParams?.headless) {
await envParamManager.verifyExpectedEnvParameters(parametersToCheck);
} else {
const missingParameters = await envParamManager.getMissingParameters(parametersToCheck);
if (missingParameters.length > 0) {
for (const { categoryName, resourceName, parameterName } of missingParameters) {
await promptMissingParameter(categoryName, resourceName, parameterName , envParamManager);
}
await envParamManager.save(); // Values must be in TPI for CFN deployment to work
}
}

// rebuild has an upstream confirmation prompt so no need to prompt again here
let continueToPush = !!context?.exeInfo?.inputParams?.yes || rebuild;

Expand Down
Loading

0 comments on commit 3112646

Please sign in to comment.