Skip to content

Commit a396f8a

Browse files
edwardfoyleakshbhu
authored andcommitted
fix: env param fail-fast checks don't fatal on missing appId or envName (aws-amplify#12373)
1 parent 4fe73f0 commit a396f8a

File tree

17 files changed

+254
-110
lines changed

17 files changed

+254
-110
lines changed

packages/amplify-category-function/src/__tests__/provider-utils/awscloudformation/utils/environmentVariablesHelper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('ensureEnvironmentVariableValues', () => {
8282

8383
prompterMock.input.mockResolvedValueOnce('testVal2').mockResolvedValueOnce('testVal3');
8484

85-
await envVarHelper.ensureEnvironmentVariableValues({ usageData: { emitError: jest.fn() } } as unknown as $TSContext);
85+
await envVarHelper.ensureEnvironmentVariableValues({ usageData: { emitError: jest.fn() } } as unknown as $TSContext, 'testAppId');
8686
expect(getEnvParamManager().getResourceParamManager('function', 'testFunc').getAllParams()).toEqual({
8787
envVarOne: 'testVal1',
8888
envVarTwo: 'testVal2',

packages/amplify-category-function/src/events/prePushHandler.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { printer } from '@aws-amplify/amplify-prompts';
12
import { $TSContext, stateManager } from 'amplify-cli-core';
23
import { categoryName } from '../constants';
34
import {
@@ -11,8 +12,25 @@ import { ensureEnvironmentVariableValues } from '../provider-utils/awscloudforma
1112
* prePush Handler event for function category
1213
*/
1314
export const prePushHandler = async (context: $TSContext): Promise<void> => {
14-
await ensureEnvironmentVariableValues(context);
15-
await ensureFunctionSecrets(context);
15+
// if appId and envName can be resolved, proceed with checking env vars and secrets
16+
const envName = stateManager.getCurrentEnvName() || context?.exeInfo?.inputParams?.amplify?.envName;
17+
// get appId from amplify-meta or fallback to input params
18+
const appId: string | undefined =
19+
(stateManager.getMeta(undefined, { throwIfNotExist: false }) || {})?.providers?.awscloudformation?.AmplifyAppId ||
20+
context?.exeInfo?.inputParams?.amplify?.appId;
21+
22+
// this handler is executed during `init --forcePush` which does an init, then a pull, then a push all in one
23+
// These parameters should always be present but it is possible they are not on init.
24+
// Hence this check will skip these checks if we can't resolve the prerequisite information
25+
if (envName && appId) {
26+
await ensureEnvironmentVariableValues(context, appId);
27+
await ensureFunctionSecrets(context);
28+
} else {
29+
printer.warn(
30+
'Could not resolve either appId, environment name or both. Skipping environment check for function secrets and environment variables',
31+
);
32+
}
33+
1634
await ensureLambdaExecutionRoleOutputs();
1735
};
1836

packages/amplify-category-function/src/provider-utils/awscloudformation/secrets/functionSecretsStateManager.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $TSContext, AmplifyError, JSONUtilities, pathManager, ResourceName } from 'amplify-cli-core';
1+
import { $TSContext, AmplifyError, JSONUtilities, pathManager, ResourceName, stateManager } from 'amplify-cli-core';
22
import { removeSecret, retainSecret, SecretDeltas, SecretName, setSecret } from '@aws-amplify/amplify-function-plugin-interface';
33
import * as path from 'path';
44
import * as fs from 'fs-extra';
@@ -105,13 +105,15 @@ export class FunctionSecretsStateManager {
105105
return;
106106
}
107107
if (!this.isInteractive()) {
108+
const inputEnvName = this.context?.exeInfo?.inputParams?.amplify?.envName;
109+
const inputAppId = this.context?.exeInfo?.inputParams?.amplify?.appId;
108110
const resolution =
109111
`Run 'amplify push' interactively to specify values.\n` +
110-
`Alternatively, manually add values in SSM ParameterStore for the following parameter names:\n\n` +
111-
`${addedSecrets.map((secretName) => getFullyQualifiedSecretName(secretName, functionName)).join('\n')}\n`;
112+
`Alternatively, manually add SecureString values in SSM Parameter Store for the following parameter names:\n\n` +
113+
`${addedSecrets.map((secretName) => getFullyQualifiedSecretName(secretName, functionName, inputEnvName, inputAppId)).join('\n')}\n`;
112114
throw new AmplifyError('EnvironmentConfigurationError', {
113115
message: `Function ${functionName} is missing secret values in this environment.`,
114-
details: `[${addedSecrets}] ${addedSecrets.length > 1 ? 'does' : 'do'} not have values.`,
116+
details: `[${addedSecrets}] ${addedSecrets.length > 1 ? 'do' : 'does'} not have values.`,
115117
resolution,
116118
link: 'https://docs.amplify.aws/cli/reference/ssm-parameter-store/#manually-creating-parameters',
117119
});
@@ -184,7 +186,10 @@ export class FunctionSecretsStateManager {
184186
* @returns string[] of all secret names for the function
185187
*/
186188
private getCloudFunctionSecretNames = async (functionName: string, envName?: string): Promise<string[]> => {
187-
const prefix = getFunctionSecretPrefix(functionName, envName);
189+
if (envName === undefined) {
190+
envName = stateManager.getCurrentEnvName() || this.context?.exeInfo?.inputParams?.amplify?.envName;
191+
}
192+
const prefix = getFunctionSecretPrefix(functionName, envName, this.context?.exeInfo?.inputParams?.amplify?.appId);
188193
const parts = path.parse(prefix);
189194
const unfilteredSecrets = await this.ssmClientWrapper.getSecretNamesByPath(parts.dir);
190195
return unfilteredSecrets.filter((secretName) => secretName.startsWith(prefix)).map((secretName) => secretName.slice(prefix.length));

packages/amplify-category-function/src/provider-utils/awscloudformation/secrets/secretName.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,27 @@ export const secretsPathAmplifyAppIdKey = 'secretsPathAmplifyAppId';
1616
*
1717
* If envName is not specified, the current env is assumed
1818
*/
19-
export const getFullyQualifiedSecretName = (secretName: string, functionName: string, envName?: string) =>
20-
`${getFunctionSecretPrefix(functionName, envName)}${secretName}`;
19+
export const getFullyQualifiedSecretName = (secretName: string, functionName: string, envName?: string, appId?: string) =>
20+
`${getFunctionSecretPrefix(functionName, envName, appId)}${secretName}`;
2121

2222
/**
2323
* Returns the SSM parameter name prefix for all secrets for the given function in the given env
2424
*
2525
* If envName is not specified, the current env is assumed
2626
*/
27-
export const getFunctionSecretPrefix = (functionName: string, envName?: string) =>
28-
path.posix.join(getEnvSecretPrefix(envName), `AMPLIFY_${functionName}_`);
27+
export const getFunctionSecretPrefix = (functionName: string, envName?: string, appId?: string) =>
28+
path.posix.join(getEnvSecretPrefix(envName, appId), `AMPLIFY_${functionName}_`);
2929

3030
/**
3131
* Returns the SSM parameter name prefix for all secrets in the given env.
3232
*
3333
* If envName is not specified, the current env is assumed
3434
*/
35-
export const getEnvSecretPrefix = (envName: string = stateManager.getLocalEnvInfo()?.envName) => {
35+
export const getEnvSecretPrefix = (envName: string = stateManager.getCurrentEnvName(), appId: string = getAppId()) => {
3636
if (!envName) {
3737
throw new Error('Could not determine the current Amplify environment name. Try running `amplify env checkout`.');
3838
}
39-
return path.posix.join('/amplify', getAppId(), envName);
39+
return path.posix.join('/amplify', appId, envName);
4040
};
4141

4242
// NOTE: Even though the following 2 functions are CFN specific, I'm putting them here to colocate all of the secret naming logic

packages/amplify-category-function/src/provider-utils/awscloudformation/utils/environmentVariablesHelper.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import path from 'path';
22
import _ from 'lodash';
33
import * as uuid from 'uuid';
4-
import { $TSContext, stateManager, pathManager, JSONUtilities, exitOnNextTick, $TSAny, $TSObject } from 'amplify-cli-core';
5-
import { byValue, formatter, maxLength, printer, prompter } from '@aws-amplify/amplify-prompts';
4+
import { $TSContext, stateManager, pathManager, JSONUtilities, $TSAny, $TSObject, AmplifyError } from 'amplify-cli-core';
5+
import { byValue, maxLength, printer, prompter } from '@aws-amplify/amplify-prompts';
66
import { getEnvParamManager, ensureEnvParamManager } from '@aws-amplify/amplify-environment-parameters';
77
import { functionParametersFileName } from './constants';
88
import { categoryName } from '../../../constants';
@@ -168,7 +168,7 @@ const askForEnvironmentVariableValue = async (
168168
/**
169169
* Ensure that values are provided for all env vars in the current environment
170170
*/
171-
export const ensureEnvironmentVariableValues = async (context: $TSContext): Promise<void> => {
171+
export const ensureEnvironmentVariableValues = async (context: $TSContext, appId: string): Promise<void> => {
172172
const yesFlagSet = context?.exeInfo?.inputParams?.yes || context?.input?.options?.yes;
173173
const currentEnvName = stateManager.localEnvInfoExists()
174174
? stateManager.getLocalEnvInfo()?.envName
@@ -198,16 +198,7 @@ export const ensureEnvironmentVariableValues = async (context: $TSContext): Prom
198198
// there are some missing env vars
199199

200200
if (yesFlagSet) {
201-
// in this case, we can't prompt for missing values, so fail gracefully
202-
const errMessage = `Cannot push Amplify environment "${currentEnvName}" due to missing Lambda function environment variable values. Rerun 'amplify push' without '--yes' to fix.`;
203-
printer.error(errMessage);
204-
const missingEnvVarsMessage = functionConfigMissingEnvVars.map(({ missingEnvVars, funcName }) => {
205-
const missingEnvVarsString = missingEnvVars.map((missing) => missing.environmentVariableName).join(', ');
206-
return `Function ${funcName} is missing values for environment variables: ${missingEnvVarsString}`;
207-
});
208-
formatter.list(missingEnvVarsMessage);
209-
await context.usageData.emitError(new Error(errMessage));
210-
exitOnNextTick(1);
201+
throw createMissingEnvVarsError(functionConfigMissingEnvVars, appId, currentEnvName);
211202
}
212203

213204
printer.info('Some Lambda function environment variables are missing values in this Amplify environment.');
@@ -322,3 +313,62 @@ const getStoredKeyValue = (resourceName: string, envName?: string): Record<strin
322313
const setStoredKeyValue = (resourceName: string, newKeyValue: $TSAny, envName?: string): void => {
323314
getEnvParamManager(envName).getResourceParamManager(categoryName, resourceName).setAllParams(newKeyValue);
324315
};
316+
317+
type MissingEnvVarsConfig = {
318+
funcName: string;
319+
missingEnvVars: {
320+
environmentVariableName: string;
321+
cloudFormationParameterName: string;
322+
}[];
323+
}[];
324+
325+
const createMissingEnvVarsError = (
326+
missingVars: MissingEnvVarsConfig,
327+
appId: string | undefined,
328+
envName: string | undefined,
329+
): AmplifyError => {
330+
const message = `This environment is missing some function environment variable values.`;
331+
const missingEnvVarsDetails = missingVars
332+
.map(({ missingEnvVars, funcName }) => {
333+
const missingEnvVarsString = missingEnvVars.map((missing) => missing.environmentVariableName).join(', ');
334+
return `Function ${funcName} is missing values for environment variables: ${missingEnvVarsString}`;
335+
})
336+
.join('\n');
337+
if (appId === undefined) {
338+
return new AmplifyError('EnvironmentConfigurationError', {
339+
message: `${message} An AppId could not be determined for fetching missing parameters.`,
340+
details: missingEnvVarsDetails,
341+
resolution: `Make sure your project is initialized and rerun 'amplify push' without '--yes' to fix.`,
342+
});
343+
}
344+
345+
if (envName === undefined) {
346+
return new AmplifyError('EnvironmentConfigurationError', {
347+
message: `${message} A current environment name could not be determined for fetching missing parameters.`,
348+
details: missingEnvVarsDetails,
349+
resolution: `Make sure your project is initialized using "amplify init"`,
350+
});
351+
}
352+
353+
// appId and envName are specified so we can provide a specific error message
354+
355+
const missingFullPaths = missingVars
356+
.map(({ missingEnvVars, funcName }) =>
357+
missingEnvVars.map((missing) => getParamKey(appId, envName, funcName, missing.cloudFormationParameterName)),
358+
)
359+
.flat();
360+
361+
const resolution =
362+
`Run 'amplify push' interactively to specify values.\n` +
363+
`Alternatively, manually add values in SSM ParameterStore for the following parameter names:\n\n` +
364+
`${missingFullPaths.join('\n')}\n`;
365+
return new AmplifyError('EnvironmentConfigurationError', {
366+
message,
367+
details: missingEnvVarsDetails,
368+
resolution,
369+
link: 'https://docs.amplify.aws/cli/reference/ssm-parameter-store/#manually-creating-parameters',
370+
});
371+
};
372+
373+
const getParamKey = (appId: string, envName: string, funcName: string, paramName: string) =>
374+
`/amplify/${appId}/${envName}/AMPLIFY_function_${funcName}_${paramName}`;

packages/amplify-cli/src/__tests__/commands/env.test.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
describe('amplify env: ', () => {
2-
const mockExit = jest.fn();
3-
jest.mock('amplify-cli-core', () => ({
4-
exitOnNextTick: mockExit,
5-
pathManager: { getAmplifyMetaFilePath: jest.fn().mockReturnValue('test_file_does_not_exist') },
6-
constants: jest.requireActual('amplify-cli-core').constants,
7-
}));
8-
const { run: runEnvCmd } = require('../../commands/env');
9-
const { run: runAddEnvCmd } = require('../../commands/env/add');
10-
const envList = require('../../commands/env/list');
11-
jest.mock('../../commands/env/list');
1+
import { $TSContext, AmplifyError } from 'amplify-cli-core';
2+
jest.mock('../../commands/init');
3+
jest.mock('amplify-cli-core');
4+
import { run as runEnvCmd } from '../../commands/env';
5+
import { run as runAddEnvCmd } from '../../commands/env/add';
6+
import * as envList from '../../commands/env/list';
7+
8+
const AmplifyErrorMock = AmplifyError as jest.MockedClass<typeof AmplifyError>;
9+
10+
AmplifyErrorMock.mockImplementation(() => new Error('test error') as AmplifyError);
1211

12+
describe('amplify env: ', () => {
1313
it('env run method should exist', () => {
1414
expect(runEnvCmd).toBeDefined();
1515
});
@@ -18,24 +18,28 @@ describe('amplify env: ', () => {
1818
expect(runAddEnvCmd).toBeDefined();
1919
});
2020

21-
it('env add method should throw if meta file does not exist', () => {
22-
expect(async () => await runAddEnvCmd()).rejects.toThrow();
21+
it('env add method should throw if meta file does not exist', async () => {
22+
await expect(runAddEnvCmd({} as $TSContext)).rejects.toThrow();
2323
});
2424

2525
it('env ls is an alias for env list', async () => {
2626
const mockEnvListRun = jest.spyOn(envList, 'run');
27-
await runEnvCmd({
28-
input: {
29-
subCommands: ['list'],
27+
const mockContext = {
28+
print: {
29+
table: jest.fn(),
30+
},
31+
amplify: {
32+
getAllEnvs: jest.fn().mockReturnValue(['testa', 'testb']),
33+
getEnvInfo: jest.fn().mockReturnValue({ envName: 'testa' }),
3034
},
31-
parameters: {},
32-
});
33-
await runEnvCmd({
3435
input: {
35-
subCommands: ['ls'],
36+
subCommands: ['list'],
3637
},
3738
parameters: {},
38-
});
39+
};
40+
await runEnvCmd(mockContext);
41+
mockContext.input.subCommands = ['ls'];
42+
await runEnvCmd(mockContext);
3943
expect(mockEnvListRun).toHaveBeenCalledTimes(2);
4044
});
4145

packages/amplify-cli/src/__tests__/utils/verify-expected-env-params.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { $TSContext } from 'amplify-cli-core';
77
jest.mock('../../commands/build');
88
jest.mock('@aws-amplify/amplify-prompts');
99
jest.mock('@aws-amplify/amplify-environment-parameters');
10+
jest.mock('@aws-amplify/amplify-provider-awscloudformation');
11+
jest.mock('amplify-cli-core');
1012

1113
const getResourcesMock = getChangedResources as jest.MockedFunction<typeof getChangedResources>;
1214
const ensureEnvParamManagerMock = ensureEnvParamManager as jest.MockedFunction<typeof ensureEnvParamManager>;
@@ -62,7 +64,7 @@ describe('verifyExpectedEnvParams', () => {
6264
});
6365
it('filters parameters based on category and resourceName if specified', async () => {
6466
await verifyExpectedEnvParams(contextStub, 'storage');
65-
expect(verifyExpectedEnvParametersMock).toHaveBeenCalledWith([resourceList[0]]);
67+
expect(verifyExpectedEnvParametersMock).toHaveBeenCalledWith([resourceList[0]], undefined, undefined);
6668
});
6769

6870
it('calls verify expected parameters if in non-interactive mode', async () => {

packages/amplify-cli/src/commands/env/list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { printEnvInfo } from '../helpers/envUtils';
88
export const run = async (context): Promise<void> => {
99
const { envName } = context.amplify.getEnvInfo();
1010

11-
if (context.parameters.options.details) {
11+
if (context?.parameters?.options?.details) {
1212
const allEnvs = context.amplify.getEnvDetails();
1313
if (context.parameters.options.json) {
1414
printer.info(JSONUtilities.stringify(allEnvs) as string);
@@ -25,7 +25,7 @@ export const run = async (context): Promise<void> => {
2525
});
2626
} else {
2727
const allEnvs = context.amplify.getAllEnvs();
28-
if (context.parameters.options.json) {
28+
if (context?.parameters?.options?.json) {
2929
printer.info(JSONUtilities.stringify({ envs: allEnvs }) as string);
3030
return;
3131
}

packages/amplify-cli/src/utils/verify-expected-env-params.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { $TSContext, IAmplifyResource, stateManager, constants } from 'amplify-c
22
import { ensureEnvParamManager, IEnvironmentParameterManager, ServiceDownloadHandler } from '@aws-amplify/amplify-environment-parameters';
33
import { printer, prompter } from '@aws-amplify/amplify-prompts';
44
import { getChangedResources, getAllResources } from '../commands/build';
5+
import { resolveAppId } from '@aws-amplify/amplify-provider-awscloudformation';
56

67
export const verifyExpectedEnvParams = async (context: $TSContext, category?: string, resourceName?: string) => {
78
const envParamManager = stateManager.localEnvInfoExists()
@@ -27,7 +28,16 @@ export const verifyExpectedEnvParams = async (context: $TSContext, category?: st
2728
});
2829

2930
if (context?.exeInfo?.inputParams?.yes || context?.exeInfo?.inputParams?.headless) {
30-
await envParamManager.verifyExpectedEnvParameters(parametersToCheck);
31+
let appId: string | undefined = undefined;
32+
try {
33+
appId = resolveAppId(context);
34+
} catch {
35+
// If AppId can't be resolved, this only affects the error message of verifyExpectedEnvParameters in the case that parameters are
36+
// actually missing. So we let appId be undefined here and verifyExpectedEnvParameters will print a different error message based
37+
// on the information available to it
38+
}
39+
const envName = stateManager.getCurrentEnvName() || context?.exeInfo?.inputParams?.amplify?.envName;
40+
await envParamManager.verifyExpectedEnvParameters(parametersToCheck, appId, envName);
3141
} else {
3242
const missingParameters = await envParamManager.getMissingParameters(parametersToCheck);
3343
if (missingParameters.length > 0) {

packages/amplify-e2e-core/src/init/amplifyPush.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export const amplifyPushIterativeRollback = (cwd: string, testingWithLatestCodeb
335335
*/
336336
export const amplifyPushMissingEnvVar = (cwd: string, newEnvVarValue: string) =>
337337
spawn(getCLIPath(), ['push'], { cwd, stripColors: true })
338-
.wait('Enter the missing environment variable value of')
338+
.wait('Enter a value for')
339339
.sendLine(newEnvVarValue)
340340
.wait('Are you sure you want to continue?')
341341
.sendYes()

0 commit comments

Comments
 (0)