Skip to content

Commit e772d23

Browse files
committed
feat: SSMProvider.getMultiple
1 parent 1fe1826 commit e772d23

File tree

4 files changed

+202
-14
lines changed

4 files changed

+202
-14
lines changed

Diff for: packages/parameters/src/BaseProvider.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { ExpirableValue } from './ExpirableValue';
55
import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants';
66
import { GetParameterError, TransformParameterError } from './Exceptions';
77
import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types';
8-
import { isSSMGetOptionsInterface } from './types/SSMProvider';
9-
import type { SSMGetOptionsInterface } from './types/SSMProvider';
8+
import { isSSMGetOptionsInterface, isSSMGetMultipleOptionsInterface } from './types/SSMProvider';
9+
import type { SSMGetOptionsInterface, SSMGetMultipleOptionsInterface } from './types/SSMProvider';
1010

1111
// These providers will be dynamically initialized on first use of the helper functions
1212
const DEFAULT_PROVIDERS: { [key: string]: BaseProvider } = {};
@@ -77,6 +77,7 @@ abstract class BaseProvider implements BaseProviderInterface {
7777
return value;
7878
}
7979

80+
public async getMultiple(path: string, options?: SSMGetMultipleOptionsInterface): Promise<undefined | Record<string, unknown>>;
8081
public async getMultiple(path: string, options?: GetMultipleOptionsInterface): Promise<undefined | Record<string, unknown>> {
8182
const configs = new GetMultipleOptions(options || {});
8283
const key = [ path, configs.transform ].toString();
@@ -89,6 +90,12 @@ abstract class BaseProvider implements BaseProviderInterface {
8990

9091
let values: Record<string, unknown> = {};
9192
try {
93+
// Type assertion is needed because the SSM provider has a different signature for the get method
94+
if (options && isSSMGetMultipleOptionsInterface(options)) {
95+
options.sdkOptions = options.sdkOptions || {};
96+
if (options.hasOwnProperty('decrypt')) options.sdkOptions.WithDecryption = options.decrypt;
97+
if (options.hasOwnProperty('recursive')) options.sdkOptions.Recursive = options.recursive;
98+
}
9299
values = await this._getMultiple(path, options?.sdkOptions);
93100
} catch (error) {
94101
throw new GetParameterError((error as Error).message);

Diff for: packages/parameters/src/SSMProvider.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BaseProvider, DEFAULT_PROVIDERS } from './BaseProvider';
2-
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
3-
import type { SSMClientConfig, GetParameterCommandInput } from '@aws-sdk/client-ssm';
2+
import { SSMClient, GetParameterCommand, paginateGetParametersByPath } from '@aws-sdk/client-ssm';
3+
import type { SSMClientConfig, GetParameterCommandInput, GetParametersByPathCommandInput } from '@aws-sdk/client-ssm';
44
import type { SSMGetOptionsInterface } from 'types/SSMProvider';
5+
import { PaginationConfiguration } from '@aws-sdk/types';
56

67
class SSMProvider extends BaseProvider {
78
public client: SSMClient;
@@ -23,8 +24,42 @@ class SSMProvider extends BaseProvider {
2324
return result.Parameter?.Value;
2425
}
2526

26-
protected _getMultiple(_path: string): Promise<Record<string, string | undefined>> {
27-
throw Error('Not implemented.');
27+
protected async _getMultiple(path: string, sdkOptions?: Partial<GetParametersByPathCommandInput>): Promise<Record<string, string | undefined>> {
28+
const options: GetParametersByPathCommandInput = {
29+
Path: path,
30+
};
31+
const paginationOptions: PaginationConfiguration = {
32+
client: this.client
33+
};
34+
if (sdkOptions) {
35+
Object.assign(options, sdkOptions);
36+
if (sdkOptions.MaxResults) {
37+
paginationOptions.pageSize = sdkOptions.MaxResults;
38+
}
39+
}
40+
41+
const parameters: Record<string, string | undefined> = {};
42+
for await (const page of paginateGetParametersByPath(paginationOptions, options)) {
43+
for (const parameter of page.Parameters || []) {
44+
/**
45+
* Standardize the parameter name
46+
*
47+
* The parameter name returned by SSM will contain the full path.
48+
* However, for readability, we should return only the part after the path.
49+
**/
50+
51+
// If the parameter is present in the response, then it has a Name
52+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
53+
let name = parameter.Name!;
54+
name = name.replace(path, '');
55+
if (name.startsWith('/')) {
56+
name = name.replace('/', '');
57+
}
58+
parameters[name] = parameter.Value;
59+
}
60+
}
61+
62+
return parameters;
2863
}
2964
}
3065

Diff for: packages/parameters/src/types/SSMProvider.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GetParameterCommandInput } from '@aws-sdk/client-ssm';
1+
import type { GetParameterCommandInput, GetParametersByPathCommandInput } from '@aws-sdk/client-ssm';
22
import type { TransformOptions } from 'types/BaseProvider';
33

44
/**
@@ -19,7 +19,24 @@ interface SSMGetOptionsInterface {
1919

2020
const isSSMGetOptionsInterface = (options: unknown): options is SSMGetOptionsInterface => (options as SSMGetOptionsInterface).decrypt !== undefined;
2121

22+
interface SSMGetMultipleOptionsInterface {
23+
maxAge?: number
24+
forceFetch?: boolean
25+
sdkOptions?: Partial<GetParametersByPathCommandInput>
26+
decrypt?: boolean
27+
recursive?: boolean
28+
transform?: string
29+
throwOnTransformError?: boolean
30+
}
31+
32+
const isSSMGetMultipleOptionsInterface =
33+
(options: unknown): options is SSMGetMultipleOptionsInterface =>
34+
(options as SSMGetMultipleOptionsInterface).decrypt !== undefined ||
35+
(options as SSMGetMultipleOptionsInterface).recursive !== undefined;
36+
2237
export {
2338
SSMGetOptionsInterface,
2439
isSSMGetOptionsInterface,
40+
SSMGetMultipleOptionsInterface,
41+
isSSMGetMultipleOptionsInterface,
2542
};

Diff for: packages/parameters/tests/unit/SSMProvider.test.ts

+136-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @group unit/parameters/SSMProvider/class
55
*/
66
import { SSMProvider } from '../../src/SSMProvider';
7-
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
7+
import { SSMClient, GetParameterCommand, GetParametersByPathCommand } from '@aws-sdk/client-ssm';
88
import { mockClient } from 'aws-sdk-client-mock';
99
import 'aws-sdk-client-mock-jest';
1010

@@ -14,7 +14,7 @@ describe('Class: SSMProvider', () => {
1414
jest.clearAllMocks();
1515
});
1616

17-
describe('Method: get', () => {
17+
describe('Method: _get', () => {
1818

1919
test('when called without sdkOptions, it gets the parameter using the name and with no decryption', async () => {
2020

@@ -47,7 +47,7 @@ describe('Class: SSMProvider', () => {
4747
const parameterName = 'foo';
4848

4949
// Act
50-
provider.get(parameterName, { sdkOptions: { WithDecryption: true } });
50+
await provider.get(parameterName, { sdkOptions: { WithDecryption: true } });
5151

5252
// Assess
5353
expect(client).toReceiveCommandWith(GetParameterCommand, {
@@ -65,7 +65,7 @@ describe('Class: SSMProvider', () => {
6565
const parameterName = 'foo';
6666

6767
// Act
68-
provider.get(parameterName, { decrypt: true });
68+
await provider.get(parameterName, { decrypt: true });
6969

7070
// Assess
7171
expect(client).toReceiveCommandWith(GetParameterCommand, {
@@ -79,13 +79,142 @@ describe('Class: SSMProvider', () => {
7979

8080
describe('Method: _getMultiple', () => {
8181

82-
test('when called throws', async () => {
82+
test('when called with only a path, it passes it to the sdk', async () => {
8383

8484
// Prepare
8585
const provider = new SSMProvider();
86+
const client = mockClient(SSMClient).on(GetParametersByPathCommand)
87+
.resolves({});
88+
const parameterPath = '/foo';
8689

87-
// Act / Assess
88-
expect(provider.getMultiple('foo')).rejects.toThrow('Not implemented.');
90+
// Act
91+
await provider.getMultiple(parameterPath);
92+
93+
// Assess
94+
expect(client).toReceiveCommandWith(GetParametersByPathCommand, {
95+
Path: parameterPath,
96+
});
97+
98+
});
99+
100+
test('when called with a path and sdkOptions, it passes them to the sdk', async () => {
101+
102+
// Prepare
103+
const provider = new SSMProvider();
104+
const client = mockClient(SSMClient).on(GetParametersByPathCommand)
105+
.resolves({
106+
Parameters: []
107+
});
108+
const parameterPath = '/foo';
109+
110+
// Act
111+
await provider.getMultiple(parameterPath, { sdkOptions: { MaxResults: 10 } });
112+
113+
// Assess
114+
expect(client).toReceiveCommandWith(GetParametersByPathCommand, {
115+
Path: parameterPath,
116+
MaxResults: 10,
117+
});
118+
119+
});
120+
121+
test('when called with no options, it uses the default sdk options', async () => {
122+
123+
// Prepare
124+
const provider = new SSMProvider();
125+
const client = mockClient(SSMClient).on(GetParametersByPathCommand)
126+
.resolves({
127+
Parameters: []
128+
});
129+
const parameterPath = '/foo';
130+
131+
// Act
132+
await provider.getMultiple(parameterPath);
133+
134+
// Assess
135+
expect(client).toReceiveCommandWith(GetParametersByPathCommand, {
136+
Path: parameterPath,
137+
});
138+
139+
});
140+
141+
test('when called with decrypt or recursive, it passes them to the sdk', async () => {
142+
143+
// Prepare
144+
const provider = new SSMProvider();
145+
const client = mockClient(SSMClient).on(GetParametersByPathCommand)
146+
.resolves({
147+
Parameters: []
148+
});
149+
const parameterPath = '/foo';
150+
151+
// Act
152+
await provider.getMultiple(parameterPath, { recursive: false, decrypt: true });
153+
154+
// Assess
155+
expect(client).toReceiveCommandWith(GetParametersByPathCommand, {
156+
Path: parameterPath,
157+
Recursive: false,
158+
WithDecryption: true,
159+
});
160+
161+
});
162+
163+
test('when multiple parameters that share the same path as suffix are retrieved, it returns an object with the names only', async () => {
164+
165+
// Prepare
166+
const provider = new SSMProvider();
167+
mockClient(SSMClient).on(GetParametersByPathCommand)
168+
.resolves({
169+
Parameters: [ {
170+
'Name':'/foo/bar',
171+
'Value':'bar',
172+
}, {
173+
'Name':'/foo/baz',
174+
'Value':'baz',
175+
} ]
176+
});
177+
const parameterPath = '/foo';
178+
179+
// Act
180+
const parameters = await provider.getMultiple(parameterPath);
181+
182+
// Assess
183+
expect(parameters).toEqual({
184+
'bar': 'bar',
185+
'baz': 'baz',
186+
});
187+
188+
});
189+
190+
test('when multiple pages are found, it returns an object with all the parameters', async () => {
191+
192+
// Prepare
193+
const provider = new SSMProvider();
194+
mockClient(SSMClient).on(GetParametersByPathCommand)
195+
.resolvesOnce({
196+
Parameters: [{
197+
'Name':'/foo/bar',
198+
'Value':'bar',
199+
}],
200+
NextToken: 'someToken',
201+
})
202+
.resolves({
203+
Parameters: [{
204+
'Name':'/foo/baz',
205+
'Value':'baz',
206+
}]
207+
});
208+
const parameterPath = '/foo';
209+
210+
// Act
211+
const parameters = await provider.getMultiple(parameterPath);
212+
213+
// Assess
214+
expect(parameters).toEqual({
215+
'bar': 'bar',
216+
'baz': 'baz',
217+
});
89218

90219
});
91220

0 commit comments

Comments
 (0)