Skip to content

Commit 44c3769

Browse files
authored
paginate getParameter call when there are more than 10 parameters (#2480)
* paginate getParameter call when there are more than 10 parameters * update changeset * remove file not needed
1 parent 56ec47e commit 44c3769

File tree

6 files changed

+164
-27
lines changed

6 files changed

+164
-27
lines changed

.changeset/famous-boats-wait.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@aws-amplify/backend-function': patch
3+
'@aws-amplify/backend': patch
4+
---
5+
6+
paginate getParameter call when there are more than 10 parameters

package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend-function/src/lambda-shims/resolve_ssm_params.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { SSM } from '@aws-sdk/client-ssm';
2-
import { after, describe, it, mock } from 'node:test';
1+
import { GetParametersCommandInput, SSM } from '@aws-sdk/client-ssm';
2+
import { afterEach, describe, it, mock } from 'node:test';
33
import { internalAmplifyFunctionResolveSsmParams } from './resolve_ssm_params.js';
44
import assert from 'node:assert';
55

@@ -8,7 +8,7 @@ void describe('internalAmplifyFunctionResolveSsmParams', () => {
88
const client = new SSM();
99

1010
// reset process.env after test suite to ensure there are no side effects
11-
after(() => {
11+
afterEach(() => {
1212
process.env = originalEnv;
1313
});
1414

@@ -43,4 +43,42 @@ void describe('internalAmplifyFunctionResolveSsmParams', () => {
4343
assert.equal(mockGetParameters.mock.callCount(), 1);
4444
assert.equal(process.env[envName], secretValue);
4545
});
46+
47+
void it('paginates when there are more than 10 secrets', async () => {
48+
const envName = 'TEST_SECRET';
49+
const secretPath = '/test/path';
50+
let ssmPaths = {};
51+
for (let i = 0; i < 100; i++) {
52+
ssmPaths = Object.assign(ssmPaths, {
53+
[secretPath + i]: {
54+
name: envName + i,
55+
sharedSecretPath: '/test/shared/path',
56+
},
57+
});
58+
}
59+
process.env.AMPLIFY_SSM_ENV_CONFIG = JSON.stringify(ssmPaths);
60+
61+
// Let's return the value same as args
62+
const mockGetParameters = mock.method(
63+
client,
64+
'getParameters',
65+
(args: GetParametersCommandInput) => {
66+
const parameters: unknown[] = [];
67+
args.Names?.forEach((name) => {
68+
parameters.push({
69+
Name: name,
70+
Value: name,
71+
});
72+
});
73+
return Promise.resolve({
74+
Parameters: parameters,
75+
});
76+
}
77+
);
78+
await internalAmplifyFunctionResolveSsmParams(client);
79+
assert.equal(mockGetParameters.mock.callCount(), 10);
80+
for (let i = 0; i < 100; i++) {
81+
assert.equal(process.env[envName + i], secretPath + i);
82+
}
83+
});
4684
});

packages/backend-function/src/lambda-shims/resolve_ssm_params.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* This code loads environment values from SSM and places them in their corresponding environment variables.
33
* If there are no SSM environment values for this function, this is a noop.
44
*/
5-
import type { SSM } from '@aws-sdk/client-ssm';
5+
import type { GetParametersCommandOutput, SSM } from '@aws-sdk/client-ssm';
66
import type { SsmEnvVars } from '../function_env_translator.js';
77

88
/**
@@ -27,11 +27,36 @@ export const internalAmplifyFunctionResolveSsmParams = async (client?: SSM) => {
2727
actualSsmClient = new ssmSdk.SSM();
2828
}
2929

30+
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
31+
const chunks: T[][] = [];
32+
for (let i = 0; i < array.length; i += chunkSize) {
33+
chunks.push(array.slice(i, i + chunkSize));
34+
}
35+
return chunks;
36+
};
37+
3038
const resolveSecrets = async (paths: string[]) => {
31-
const response = await actualSsmClient.getParameters({
32-
Names: paths,
33-
WithDecryption: true,
34-
});
39+
const response = (
40+
await Promise.all(
41+
chunkArray(paths, 10).map(
42+
async (chunkedPaths) =>
43+
await actualSsmClient.getParameters({
44+
Names: chunkedPaths,
45+
WithDecryption: true,
46+
})
47+
)
48+
)
49+
).reduce(
50+
(accumulator, res: GetParametersCommandOutput) => {
51+
accumulator.Parameters?.push(...(res.Parameters ?? []));
52+
accumulator.InvalidParameters?.push(...(res.InvalidParameters ?? []));
53+
return accumulator;
54+
},
55+
{
56+
Parameters: [],
57+
InvalidParameters: [],
58+
} as Partial<GetParametersCommandOutput>
59+
);
3560

3661
if (response.Parameters && response.Parameters.length > 0) {
3762
for (const parameter of response.Parameters) {

packages/backend-function/src/lambda-shims/resolve_ssm_params_sdk_v2.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import aws from 'aws-sdk';
1+
import aws, { SSM } from 'aws-sdk';
22
import { after, describe, it, mock } from 'node:test';
33
import { internalAmplifyFunctionResolveSsmParams } from './resolve_ssm_params_sdk_v2.js';
44
import assert from 'node:assert';
@@ -45,4 +45,45 @@ void describe('internalAmplifyFunctionResolveSsmParams', () => {
4545
assert.equal(mockGetParameters.mock.callCount(), 1);
4646
assert.equal(process.env[envName], secretValue);
4747
});
48+
49+
void it('paginates when there are more than 10 secrets', async () => {
50+
const envName = 'TEST_SECRET';
51+
const secretPath = '/test/path';
52+
let ssmPaths = {};
53+
for (let i = 0; i < 100; i++) {
54+
ssmPaths = Object.assign(ssmPaths, {
55+
[secretPath + i]: {
56+
name: envName + i,
57+
sharedSecretPath: '/test/shared/path',
58+
},
59+
});
60+
}
61+
process.env.AMPLIFY_SSM_ENV_CONFIG = JSON.stringify(ssmPaths);
62+
63+
// Let's return the value same as args
64+
const mockGetParameters = mock.method(
65+
client,
66+
'getParameters',
67+
(args: SSM.Types.GetParametersRequest) => {
68+
const parameters: unknown[] = [];
69+
args.Names?.forEach((name) => {
70+
parameters.push({
71+
Name: name,
72+
Value: name,
73+
});
74+
});
75+
return {
76+
promise: () =>
77+
Promise.resolve({
78+
Parameters: parameters,
79+
}),
80+
};
81+
}
82+
);
83+
await internalAmplifyFunctionResolveSsmParams(client);
84+
assert.equal(mockGetParameters.mock.callCount(), 10);
85+
for (let i = 0; i < 100; i++) {
86+
assert.equal(process.env[envName + i], secretPath + i);
87+
}
88+
});
4889
});

packages/backend-function/src/lambda-shims/resolve_ssm_params_sdk_v2.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,40 @@ export const internalAmplifyFunctionResolveSsmParams = async (
3636
return;
3737
}
3838

39+
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
40+
const chunks: T[][] = [];
41+
for (let i = 0; i < array.length; i += chunkSize) {
42+
chunks.push(array.slice(i, i + chunkSize));
43+
}
44+
return chunks;
45+
};
46+
3947
const resolveSecrets = async (paths: string[]) => {
40-
const response = await client
41-
.getParameters({
42-
Names: paths,
43-
WithDecryption: true,
44-
})
45-
.promise();
48+
const response = (
49+
await Promise.all(
50+
chunkArray(paths, 10).map(
51+
async (chunkedPaths) =>
52+
await client
53+
.getParameters({
54+
Names: chunkedPaths,
55+
WithDecryption: true,
56+
})
57+
.promise()
58+
)
59+
)
60+
).reduce(
61+
(accumulator, response) => {
62+
accumulator.Parameters?.push(...(response.Parameters ?? []));
63+
accumulator.InvalidParameters?.push(
64+
...(response.InvalidParameters ?? [])
65+
);
66+
return accumulator;
67+
},
68+
{
69+
Parameters: [],
70+
InvalidParameters: [],
71+
} as SSM.GetParametersResult
72+
);
4673

4774
if (response.Parameters && response.Parameters.length > 0) {
4875
for (const parameter of response.Parameters) {

0 commit comments

Comments
 (0)