From 16cd976482e56ffd0463850924590988edf343d8 Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Tue, 3 Sep 2024 12:11:43 +0200 Subject: [PATCH 01/10] feat: adds implementation for setParameter --- packages/parameters/src/ssm/SSMProvider.ts | 61 ++++++++++ packages/parameters/src/ssm/index.ts | 1 + packages/parameters/src/ssm/setParameter.ts | 107 +++++++++++++++++ packages/parameters/src/types/SSMProvider.ts | 40 +++++++ .../parameters/tests/unit/SSMProvider.test.ts | 90 ++++++++++++++ .../tests/unit/setParameter.test.ts | 110 ++++++++++++++++++ 6 files changed, 409 insertions(+) create mode 100644 packages/parameters/src/ssm/setParameter.ts create mode 100644 packages/parameters/tests/unit/setParameter.test.ts diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index d1a1a56412..c38a049a5b 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -2,6 +2,7 @@ import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { GetParameterCommand, GetParametersCommand, + PutParameterCommand, SSMClient, paginateGetParametersByPath, } from '@aws-sdk/client-ssm'; @@ -10,6 +11,7 @@ import type { GetParametersByPathCommandInput, GetParametersCommandInput, GetParametersCommandOutput, + PutParameterCommandInput, SSMPaginationConfiguration, } from '@aws-sdk/client-ssm'; import { BaseProvider } from '../base/BaseProvider.js'; @@ -26,6 +28,7 @@ import type { SSMGetParametersByNameOutput, SSMGetParametersByNameOutputInterface, SSMProviderOptions, + SSMSetOptions, SSMSplitBatchAndDecryptParametersOutputType, } from '../types/SSMProvider.js'; @@ -322,6 +325,45 @@ class SSMProvider extends BaseProvider { >; } + /** + * Sets a parameter in AWS Systems Manager (SSM). + * + * @example + * ```typescript + * import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm'; + * + * const parametersProvider = new SSMProvider(); + * + * export const handler = async (): Promise => { + * // Set a parameter in SSM + * const version = await parametersProvider.set('/my-parameter', { value: 'my-value' }); + * console.log(`Parameter version: ${version}`); + * }; + * ``` + * + * You can customize the storage of the value by passing options to the function: + * * `value` - The value of the parameter, which is a mandatory option. + * * `overwrite` - Whether to overwrite the value if it already exists (default: `false`) + * * `description` - The description of the parameter + * * `parameterType` - The type of the parameter, can be one of `String`, `StringList`, or `SecureString` (default: `String`) + * * `tier` - The parameter tier to use, can be one of `Standard`, `Advanced`, and `Intelligent-Tiering` (default: `Standard`) + * * `kmsKeyId` - The KMS key id to use to encrypt the parameter + * * `sdkOptions` - Extra options to pass to the AWS SDK v3 for JavaScript client + * + * @param {string} name - The name of the parameter + * @param {SSMSetOptions} options - Options to configure the parameter + * @returns {Promise} The version of the parameter + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/ + */ + public async set< + InferredFromOptionsType extends SSMSetOptions | undefined = SSMSetOptions, + >( + name: string, + options: InferredFromOptionsType & SSMSetOptions + ): Promise { + return this._set(name, options); + } + /** * Retrieve multiple values from AWS Systems Manager. * @@ -790,6 +832,25 @@ class SSMProvider extends BaseProvider { return undefined; } + protected async _set( + name: string, + options: SSMSetOptions + ): Promise { + const sdkOptions: PutParameterCommandInput = { + Tier: options.tier || 'Standard', + Type: options.parameterType || 'String', + Overwrite: options.overwrite || false, + ...(options.kmsKeyId ? { KeyId: options.kmsKeyId } : {}), + ...(options.description ? { Description: options.description } : {}), + ...(options?.sdkOptions || {}), + Name: name, + Value: options.value, + }; + const result = await this.client.send(new PutParameterCommand(sdkOptions)); + + return result.Version; + } + /** * Split parameters that can be fetched by GetParameters vs GetParameter. * diff --git a/packages/parameters/src/ssm/index.ts b/packages/parameters/src/ssm/index.ts index ffa9aba3a9..a4459d0c18 100644 --- a/packages/parameters/src/ssm/index.ts +++ b/packages/parameters/src/ssm/index.ts @@ -1,4 +1,5 @@ export { SSMProvider } from './SSMProvider.js'; export { getParameter } from './getParameter.js'; +export { setParameter } from './setParameter.js'; export { getParameters } from './getParameters.js'; export { getParametersByName } from './getParametersByName.js'; diff --git a/packages/parameters/src/ssm/setParameter.ts b/packages/parameters/src/ssm/setParameter.ts new file mode 100644 index 0000000000..a585303a4d --- /dev/null +++ b/packages/parameters/src/ssm/setParameter.ts @@ -0,0 +1,107 @@ +import { DEFAULT_PROVIDERS } from '../base/DefaultProviders.js'; +import type { SSMGetOptions, SSMSetOptions } from '../types/SSMProvider'; +import { SSMProvider } from './SSMProvider'; + +/** + * ## Intro + * The Parameters utility provides an SSMProvider that allows setting parameters in AWS Systems Manager. + * + * ## Getting started + * + * This utility supports AWS SDK v3 for JavaScript only. This allows the utility to be modular, and you to install only + * the SDK packages you need and keep your bundle size small. + * + * To use the provider, you must install the Parameters utility and the AWS SDK v3 for JavaScript for SSM: + * + * ```sh + * npm install @aws-lambda-powertools/parameters @aws-sdk/client-ssm + *``` + * + * ## Basic Usage + * + * @example + * ```typescript + * import { setParameter } from '@aws-lambda-powertools/parameters/ssm'; + * + * export const handler = async (): Promise => { + * // Set a parameter + * const version = await setParameter('/my-parameter', { value: 'my-value' }); + * console.log(Parameter version: ${version}); + * }; + * ``` + * + * ## Advanced Usage + * + * ### Overwriting a parameter + * + * By default, the provider will not overwrite a parameter if it already exists. You can force the provider to overwrite the parameter by using the `overwrite` option. + * + * @example + * ```typescript + * import { setParameter } from '@aws-lambda-powertools/parameters/ssm'; + * + * export const handler = async (): Promise => { + * // Set a parameter and overwrite it + * const version = await setParameter('/my-parameter', { + * value: 'my-value', + * overwrite: true, + * }); + * console.log(Parameter version: ${version}); + * }; + * ``` + * + * ### Extra SDK options + * + * When setting a parameter, you can pass extra options to the AWS SDK v3 for JavaScript client by using the sdkOptions parameter. + * + * @example + * ```typescript + * import { setParameter } from '@aws-lambda-powertools/parameters/ssm'; + * + * export const handler = async (): Promise => { + * // Set a parameter with extra options + * const version = await setParameter('/my-parameter', { + * value: 'my-value', + * sdkOptions: { + * Overwrite: true, + * }, + * }); + * }; + * ``` + * + * This object accepts the same options as the AWS SDK v3 for JavaScript `PutParameterCommandInput` interface. + * + * ### Built-in provider class + * + * For greater flexibility such as configuring the underlying SDK client used by built-in providers, you can use the {@link SSMProvider} class. + * + * ### Options + * + * You can customize the storage of the value by passing options to the function: + * * `value` - The value of the parameter, which is a mandatory option. + * * `overwrite` - Whether to overwrite the value if it already exists (default: `false`) + * * `description` - The description of the parameter + * * `parameterType` - The type of the parameter, can be one of `String`, `StringList`, or `SecureString` (default: `String`) + * * `tier` - The parameter tier to use, can be one of `Standard`, `Advanced`, and `Intelligent-Tiering` (default: `Standard`) + * * `kmsKeyId` - The KMS key id to use to encrypt the parameter + * * `sdkOptions` - Extra options to pass to the AWS SDK v3 for JavaScript client + * + * For more usage examples, see [our documentation](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/). + * + * @param {string} name - Name of the parameter + * @param {SSMSetOptions} options - Options to configure the parameter + * @see https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/ + */ +const setParameter = async < + InferredFromOptionsType extends SSMSetOptions | undefined = SSMSetOptions, +>( + name: string, + options: InferredFromOptionsType & SSMSetOptions +): Promise => { + if (!Object.hasOwn(DEFAULT_PROVIDERS, 'ssm')) { + DEFAULT_PROVIDERS.ssm = new SSMProvider(); + } + return (DEFAULT_PROVIDERS.ssm as SSMProvider).set(name, options); +}; + +export { setParameter }; diff --git a/packages/parameters/src/types/SSMProvider.ts b/packages/parameters/src/types/SSMProvider.ts index b647fe29c9..889438db7e 100644 --- a/packages/parameters/src/types/SSMProvider.ts +++ b/packages/parameters/src/types/SSMProvider.ts @@ -2,6 +2,7 @@ import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import type { GetParameterCommandInput, GetParametersByPathCommandInput, + PutParameterCommandInput, SSMClient, SSMClientConfig, } from '@aws-sdk/client-ssm'; @@ -94,6 +95,44 @@ type SSMGetOptions = | SSMGetOptionsTransformNone | undefined; +type ParameterType = 'String' | 'StringList' | 'SecureString'; + +type ParameterTier = 'Standard' | 'Advanced' | 'Intelligent-Tiering'; + +type SSMSetOptions = { + /** + * The parameter value + */ + value: string; + /** + * If the parameter value should be overwritten + * @default false + */ + overwrite?: boolean; + /** + * The description of the parameter + */ + description?: string; + /** + * Type of the parameter, can be one of `String`, `StringList`, or `SecureString` + * @default `String` + */ + parameterType?: ParameterType; + /** + * The parameter tier to use, can be one of `Standard`, `Advanced`, and `Intelligent-Tiering` + * @default `Standard` + */ + tier?: ParameterTier; + /** + * The KMS key id to use to encrypt the parameter + */ + kmsKeyId?: string; + /** + * Additional options to pass to the AWS SDK v3 client + */ + sdkOptions?: Partial; +}; + /** * Generic output type for the SSMProvider get method. */ @@ -239,6 +278,7 @@ type SSMGetParametersByNameOutput = export type { SSMProviderOptions, SSMGetOptions, + SSMSetOptions, SSMGetOutput, SSMGetMultipleOptions, SSMGetMultipleOutput, diff --git a/packages/parameters/tests/unit/SSMProvider.test.ts b/packages/parameters/tests/unit/SSMProvider.test.ts index ee9edc217a..ba926ef5db 100644 --- a/packages/parameters/tests/unit/SSMProvider.test.ts +++ b/packages/parameters/tests/unit/SSMProvider.test.ts @@ -7,6 +7,7 @@ import { GetParameterCommand, GetParametersByPathCommand, GetParametersCommand, + PutParameterCommand, SSMClient, } from '@aws-sdk/client-ssm'; import type { GetParametersCommandOutput } from '@aws-sdk/client-ssm'; @@ -21,6 +22,7 @@ import type { SSMGetParametersByNameOptions, SSMGetParametersByNameOutputInterface, SSMProviderOptions, + SSMSetOptions, SSMSplitBatchAndDecryptParametersOutputType, } from '../../src/types/SSMProvider.js'; @@ -1310,4 +1312,92 @@ describe('Class: SSMProvider', () => { }); }); }); + + describe('Method: _set', () => { + test('sets a parameter successfully', async () => { + const provider: SSMProvider = new SSMProvider(); + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + const parameterName: string = '/my-parameter'; + const options: SSMSetOptions = { value: 'my-value' }; + + const version = await provider.set(parameterName, options); + + expect(version).toBe(1); + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + }); + }); + + test('returns undefined if version is undefined', async () => { + const provider: SSMProvider = new SSMProvider(); + const client = mockClient(SSMClient).on(PutParameterCommand).resolves({}); + const parameterName: string = '/my-parameter'; + const options: SSMSetOptions = { value: 'my-value' }; + + const version = await provider.set(parameterName, options); + + expect(version).toBeUndefined(); + }); + + test('sets a parameter with sdk options successfully', async () => { + const provider: SSMProvider = new SSMProvider(); + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + const parameterName: string = '/my-parameter'; + const options: SSMSetOptions = { + value: 'my-value', + sdkOptions: { Overwrite: true }, + }; + + const version = await provider.set(parameterName, options); + + expect(version).toBe(1); + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + Overwrite: true, + }); + }); + + test('throws an error if setting a parameter fails', async () => { + const provider: SSMProvider = new SSMProvider(); + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .rejects(new Error('Failed to set parameter')); + const parameterName: string = '/my-parameter'; + const options: SSMSetOptions = { value: 'my-value' }; + + await expect(provider.set(parameterName, options)).rejects.toThrow( + 'Failed to set parameter' + ); + }); + + test.each([ + ['overwrite', true, 'Overwrite'], + ['description', 'my-description', 'Description'], + ['parameterType', 'SecureString', 'Type'], + ['tier', 'Advanced', 'Tier'], + ['kmsKeyId', 'my-key-id', 'KeyId'], + ])('sets a parameter with %s option', async (key, value, sdkKey) => { + const provider: SSMProvider = new SSMProvider(); + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + const parameterName: string = '/my-parameter'; + const options: SSMSetOptions = { value: 'my-value', [key]: value }; + + const version = await provider.set(parameterName, options); + + expect(version).toBe(1); + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + [sdkKey]: value, + }); + }); + }); }); diff --git a/packages/parameters/tests/unit/setParameter.test.ts b/packages/parameters/tests/unit/setParameter.test.ts new file mode 100644 index 0000000000..3936a6b143 --- /dev/null +++ b/packages/parameters/tests/unit/setParameter.test.ts @@ -0,0 +1,110 @@ +/** + * Test setParameter function + * + * @group unit/parameters/ssm/setParameter/function + */ +import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; +import { mockClient } from 'aws-sdk-client-mock'; +import { DEFAULT_PROVIDERS } from '../../src/base'; +import { SSMProvider } from '../../src/ssm/SSMProvider'; +import { setParameter } from '../../src/ssm/setParameter'; +import 'aws-sdk-client-mock-jest'; +import type { SSMSetOptions } from '../../src/types/SSMProvider'; + +describe('Function: setParameter', () => { + const parameterName = '/my-parameter'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('when called and a default provider does not exist, it instantiates one and sets the parameter', async () => { + const options: SSMSetOptions = { value: 'my-value' }; + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + + const version = await setParameter(parameterName, options); + + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + }); + expect(version).toBe(1); + }); + + test('when called and a default provider exists, it uses it and sets the parameter', async () => { + const provider = new SSMProvider(); + DEFAULT_PROVIDERS.ssm = provider; + const options: SSMSetOptions = { value: 'my-value' }; + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + + const version = await setParameter(parameterName, options); + + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + }); + expect(version).toBe(1); + expect(DEFAULT_PROVIDERS.ssm).toBe(provider); + }); + + test('when called and setting a parameter returns an undefined version, it returns undefined', async () => { + const options: SSMSetOptions = { value: 'my-value' }; + const client = mockClient(SSMClient).on(PutParameterCommand).resolves({}); + + const version = await setParameter(parameterName, options); + + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + }); + expect(version).toBeUndefined(); + }); + + test('when called with additional sdk options, it sets the parameter with the sdk options successfully', async () => { + const options: SSMSetOptions = { + value: 'my-value', + sdkOptions: { Overwrite: true }, + }; + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + + const version = await setParameter(parameterName, options); + + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + Overwrite: true, + }); + expect(version).toBe(1); + }); + + test.each([ + ['overwrite', true, 'Overwrite'], + ['description', 'my-description', 'Description'], + ['parameterType', 'SecureString', 'Type'], + ['tier', 'Advanced', 'Tier'], + ['kmsKeyId', 'my-key-id', 'KeyId'], + ])( + 'when called with %s option, it sets the parameter with the option successfully', + async (option, value, sdkOption) => { + const options: SSMSetOptions = { value: 'my-value', [option]: value }; + const client = mockClient(SSMClient) + .on(PutParameterCommand) + .resolves({ Version: 1 }); + + const version = await setParameter(parameterName, options); + + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + [sdkOption]: value, + }); + expect(version).toBe(1); + } + ); +}); From 15603359a0d31f4c75330917a56a16ffd225a401 Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Tue, 3 Sep 2024 13:20:02 +0200 Subject: [PATCH 02/10] chore: changes due to review --- packages/parameters/src/ssm/SSMProvider.ts | 8 ++++---- packages/parameters/src/ssm/setParameter.ts | 2 +- packages/parameters/tests/unit/SSMProvider.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index c38a049a5b..fcaac920a8 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -837,12 +837,12 @@ class SSMProvider extends BaseProvider { options: SSMSetOptions ): Promise { const sdkOptions: PutParameterCommandInput = { - Tier: options.tier || 'Standard', - Type: options.parameterType || 'String', - Overwrite: options.overwrite || false, + Tier: options.tier ?? 'Standard', + Type: options.parameterType ?? 'String', + Overwrite: options.overwrite ?? false, ...(options.kmsKeyId ? { KeyId: options.kmsKeyId } : {}), ...(options.description ? { Description: options.description } : {}), - ...(options?.sdkOptions || {}), + ...(options?.sdkOptions ?? {}), Name: name, Value: options.value, }; diff --git a/packages/parameters/src/ssm/setParameter.ts b/packages/parameters/src/ssm/setParameter.ts index a585303a4d..1bf7214c02 100644 --- a/packages/parameters/src/ssm/setParameter.ts +++ b/packages/parameters/src/ssm/setParameter.ts @@ -1,5 +1,5 @@ import { DEFAULT_PROVIDERS } from '../base/DefaultProviders.js'; -import type { SSMGetOptions, SSMSetOptions } from '../types/SSMProvider'; +import type { SSMSetOptions } from '../types/SSMProvider'; import { SSMProvider } from './SSMProvider'; /** diff --git a/packages/parameters/tests/unit/SSMProvider.test.ts b/packages/parameters/tests/unit/SSMProvider.test.ts index ba926ef5db..e5bb5fd268 100644 --- a/packages/parameters/tests/unit/SSMProvider.test.ts +++ b/packages/parameters/tests/unit/SSMProvider.test.ts @@ -1333,7 +1333,7 @@ describe('Class: SSMProvider', () => { test('returns undefined if version is undefined', async () => { const provider: SSMProvider = new SSMProvider(); - const client = mockClient(SSMClient).on(PutParameterCommand).resolves({}); + mockClient(SSMClient).on(PutParameterCommand).resolves({}); const parameterName: string = '/my-parameter'; const options: SSMSetOptions = { value: 'my-value' }; @@ -1365,7 +1365,7 @@ describe('Class: SSMProvider', () => { test('throws an error if setting a parameter fails', async () => { const provider: SSMProvider = new SSMProvider(); - const client = mockClient(SSMClient) + mockClient(SSMClient) .on(PutParameterCommand) .rejects(new Error('Failed to set parameter')); const parameterName: string = '/my-parameter'; From 282273626551efec439a9e4f25dd68da3600c47d Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Tue, 3 Sep 2024 13:42:11 +0200 Subject: [PATCH 03/10] docs: adds documentation --- docs/utilities/parameters.md | 19 ++++++++++++++++++- examples/snippets/parameters/setParameter.ts | 7 +++++++ .../parameters/setParameterOverwrite.ts | 10 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/parameters/setParameter.ts create mode 100644 examples/snippets/parameters/setParameterOverwrite.ts diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 3507c515f2..b06f0c3668 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -61,6 +61,7 @@ This utility requires additional permissions to work as expected. | SSM | **`getParameters`**, **`SSMProvider.getMultiple`** | **`ssm:GetParametersByPath`** | | SSM | **`getParametersByName`**, **`SSMProvider.getParametersByName`** | **`ssm:GetParameter`** and **`ssm:GetParameters`** | | SSM | If using **`decrypt: true`** | You must add an additional permission **`kms:Decrypt`** | +| SSM | **`setParameter`**, **`SSMProvider.set`** | **`ssm:PutParameter`** | | Secrets | **`getSecret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** | | DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** | | DynamoDB | **`DynamoDBProvider.getMultiple`** | **`dynamodb:Query`** | @@ -104,6 +105,20 @@ For multiple parameters, you can use either: --8<-- "examples/snippets/parameters/getParametersByNameGracefulErrorHandling.ts" ``` +### Storing parameters + +You can store parameters in the System Manager Parameter Store using `setParameter`. + +```typescript hl_lines="1 5" title="Storing a parameter in SSM" +--8<-- "examples/snippets/parameters/setParameter.ts" +``` + +If the parameter is already existent, it needs to have the `overwrite` parameter set to `true` to update the value. + +```typescript hl_lines="1 7" title="Overwriting an existing parameter in SSM" +--8<-- "examples/snippets/parameters/setParameterOverwrite.ts" +``` + ### Fetching secrets You can fetch secrets stored in Secrets Manager using `getSecret`. @@ -370,11 +385,13 @@ You can use a special `sdkOptions` object argument to pass any supported option Here is the mapping between this utility's functions and methods and the underlying SDK: | Provider | Function/Method | Client name | Function name | -| ------------------- | ------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ------------------- |--------------------------------| --------------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | SSM Parameter Store | `getParameter` | `@aws-sdk/client-ssm` | [GetParameterCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/GetParameterCommand/){target="_blank"} | | SSM Parameter Store | `getParameters` | `@aws-sdk/client-ssm` | [GetParametersByPathCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/GetParametersByPathCommand/){target="_blank"} | | SSM Parameter Store | `SSMProvider.get` | `@aws-sdk/client-ssm` | [GetParameterCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/GetParameterCommand/){target="_blank"} | | SSM Parameter Store | `SSMProvider.getMultiple` | `@aws-sdk/client-ssm` | [GetParametersByPathCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/GetParametersByPathCommand){target="_blank"} | +| SSM Parameter Store | `setParameter` | `@aws-sdk/client-ssm` | [PutParameterCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/PutParameterCommand/){target="_blank"} | +| SSM Parameter Store | `SSMProvider.set` | `@aws-sdk/client-ssm` | [PutParameterCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/command/PutParameterCommand/){target="_blank"} | | Secrets Manager | `getSecret` | `@aws-sdk/client-secrets-manager` | [GetSecretValueCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/command/GetSecretValueCommand/){target="_blank"} | | Secrets Manager | `SecretsProvider.get` | `@aws-sdk/client-secrets-manager` | [GetSecretValueCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/command/GetSecretValueCommand/){target="_blank"} | | AppConfig | `AppConfigProvider.get` | `@aws-sdk/client-appconfigdata` | [StartConfigurationSessionCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/appconfigdata/command/StartConfigurationSessionCommand/){target="_blank"} & [GetLatestConfigurationCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/appconfigdata/command/GetLatestConfigurationCommand/){target="_blank"} | diff --git a/examples/snippets/parameters/setParameter.ts b/examples/snippets/parameters/setParameter.ts new file mode 100644 index 0000000000..d8de0004e6 --- /dev/null +++ b/examples/snippets/parameters/setParameter.ts @@ -0,0 +1,7 @@ +import { setParameter } from '@aws-lambda-powertools/parameters/ssm'; + +export const handler = async (): Promise => { + // Store a string parameter + const parameter = await setParameter('/my/parameter', { value: 'my-value' }); + console.log(parameter); +}; diff --git a/examples/snippets/parameters/setParameterOverwrite.ts b/examples/snippets/parameters/setParameterOverwrite.ts new file mode 100644 index 0000000000..bb6f6f448d --- /dev/null +++ b/examples/snippets/parameters/setParameterOverwrite.ts @@ -0,0 +1,10 @@ +import { setParameter } from '@aws-lambda-powertools/parameters/ssm'; + +export const handler = async (): Promise => { + // Overwrite a string parameter + const parameter = await setParameter('/my/parameter', { + value: 'my-value', + overwrite: true, + }); + console.log(parameter); +}; From de3ac4f5eb7cc64a66d6fe86be70e4d68923907d Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Tue, 3 Sep 2024 14:42:28 +0200 Subject: [PATCH 04/10] chore: adds end-to-end-tests --- .../ssmProvider.class.test.functionCode.ts | 25 +++++++++++++++ .../tests/e2e/ssmProvider.class.test.ts | 32 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts index 01429bd9a2..6c6c6e13c1 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts @@ -5,6 +5,7 @@ import type { SSMGetMultipleOptions, SSMGetOptions, SSMGetParametersByNameOptions, + SSMSetOptions, } from '../../src/types/SSMProvider.js'; import { middleware } from '../helpers/sdkMiddlewareRequestCounter.js'; import { TinyLogger } from '../helpers/tinyLogger.js'; @@ -22,6 +23,7 @@ const providerWithMiddleware = new SSMProvider({ const paramA = process.env.PARAM_A ?? 'my-param'; const paramB = process.env.PARAM_B ?? 'my-param'; +const paramC = process.env.PARAM_C ?? 'my-param'; const paramEncryptedA = process.env.PARAM_ENCRYPTED_A ?? 'my-encrypted-param'; const paramEncryptedB = process.env.PARAM_ENCRYPTED_B ?? 'my-encrypted-param'; @@ -108,6 +110,24 @@ const _call_get_parameters_by_name = async ( } }; +const _call_set = async ( + paramName: string, + testName: string, + options: SSMSetOptions, + provider?: SSMProvider +): Promise => { + try { + const currentProvider = resolveProvider(provider); + + await currentProvider.set(paramName, options); + } catch (err) { + logger.log({ + test: testName, + error: (err as Error).message, + }); + } +}; + export const handler = async ( _event: unknown, _context: Context @@ -197,4 +217,9 @@ export const handler = async ( error: (err as Error).message, }); } + + // Test 10 + // set and overwrite parameter + await _call_set(paramC, 'set', { value: 'overwritten', overwrite: true }); + await _call_get(paramC, 'set'); }; diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index cea69b6df6..2443616a77 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -69,6 +69,9 @@ import { * Test 9 * get parameter twice, but force fetch 2nd time, we count number of SDK requests and * check that we made two API calls + * + * Test 10 + * store and overwrite a single parameter */ describe('Parameters E2E tests, SSM provider', () => { const testStack = new TestStack({ @@ -89,6 +92,7 @@ describe('Parameters E2E tests, SSM provider', () => { let paramB: string; const paramAValue = 'foo'; const paramBValue = 'bar'; + const paramCValue = 'baz'; let paramEncryptedA: string; let paramEncryptedB: string; const paramEncryptedAValue = 'foo-encrypted'; @@ -162,6 +166,19 @@ describe('Parameters E2E tests, SSM provider', () => { parameterEncryptedB.parameterName ); + const parameterSetC = new TestStringParameter( + testStack, + { + stringValue: paramCValue, + }, + { + nameSuffix: 'set/b', + } + ); + parameterSetC.grantWrite(testFunction); + parameterSetC.grantRead(testFunction); + testFunction.addEnvironment('PARAM_C', parameterSetC.parameterName); + // Deploy the stack await testStack.deploy(); @@ -350,6 +367,21 @@ describe('Parameters E2E tests, SSM provider', () => { }, TEST_CASE_TIMEOUT ); + + // Test 10 - store and overwrite single parameter + it( + 'should store and overwrite single parameter', + async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[9]); + + expect(testLog).toStrictEqual({ + test: 'set', + value: 'overwritten', + }); + }, + TEST_CASE_TIMEOUT + ); }); afterAll(async () => { From ffddb693e7268b5731bbc195734a232edde976fc Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Tue, 3 Sep 2024 14:43:57 +0200 Subject: [PATCH 05/10] chore: changes name suffix of param --- packages/parameters/tests/e2e/ssmProvider.class.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index 2443616a77..bb5618a198 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -172,7 +172,7 @@ describe('Parameters E2E tests, SSM provider', () => { stringValue: paramCValue, }, { - nameSuffix: 'set/b', + nameSuffix: 'set/c', } ); parameterSetC.grantWrite(testFunction); From e0ebb211ea678e6773a7705ca07944ca236cb073 Mon Sep 17 00:00:00 2001 From: Joshua Weber Date: Mon, 9 Sep 2024 11:12:41 +0200 Subject: [PATCH 06/10] chore: adds changes due to review --- packages/parameters/src/errors.ts | 12 +++- packages/parameters/src/ssm/SSMProvider.ts | 46 ++++++++-------- packages/parameters/src/ssm/setParameter.ts | 2 +- .../parameters/tests/unit/SSMProvider.test.ts | 15 +---- .../tests/unit/setParameter.test.ts | 55 +++++++++++-------- 5 files changed, 69 insertions(+), 61 deletions(-) diff --git a/packages/parameters/src/errors.ts b/packages/parameters/src/errors.ts index fab94a7702..5f5325c141 100644 --- a/packages/parameters/src/errors.ts +++ b/packages/parameters/src/errors.ts @@ -8,6 +8,16 @@ class GetParameterError extends Error { } } +/** + * Error thrown when a parameter cannot be set. + */ +class SetParameterError extends Error { + public constructor(message?: string) { + super(message); + this.name = 'SetParameterError'; + } +} + /** * Error thrown when a transform fails. */ @@ -19,4 +29,4 @@ class TransformParameterError extends Error { } } -export { GetParameterError, TransformParameterError }; +export { GetParameterError, TransformParameterError, SetParameterError }; diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index fcaac920a8..e3a0413475 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -12,12 +12,13 @@ import type { GetParametersCommandInput, GetParametersCommandOutput, PutParameterCommandInput, + PutParameterCommandOutput, SSMPaginationConfiguration, } from '@aws-sdk/client-ssm'; import { BaseProvider } from '../base/BaseProvider.js'; import { transformValue } from '../base/transformValue.js'; import { DEFAULT_MAX_AGE_SECS } from '../constants.js'; -import { GetParameterError } from '../errors.js'; +import { GetParameterError, SetParameterError } from '../errors.js'; import type { SSMGetMultipleOptions, SSMGetMultipleOutput, @@ -352,7 +353,7 @@ class SSMProvider extends BaseProvider { * * @param {string} name - The name of the parameter * @param {SSMSetOptions} options - Options to configure the parameter - * @returns {Promise} The version of the parameter + * @returns {Promise} The version of the parameter * @see https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/ */ public async set< @@ -360,8 +361,26 @@ class SSMProvider extends BaseProvider { >( name: string, options: InferredFromOptionsType & SSMSetOptions - ): Promise { - return this._set(name, options); + ): Promise { + const sdkOptions: PutParameterCommandInput = { + Tier: options.tier ?? 'Standard', + Type: options.parameterType ?? 'String', + Overwrite: options.overwrite ?? false, + ...(options.kmsKeyId ? { KeyId: options.kmsKeyId } : {}), + ...(options.description ? { Description: options.description } : {}), + ...(options?.sdkOptions ?? {}), + Name: name, + Value: options.value, + }; + let result: PutParameterCommandOutput; + try { + result = await this.client.send(new PutParameterCommand(sdkOptions)); + } catch (error) { + throw new SetParameterError(`Unable to set parameter with name ${name}`); + } + + // biome-ignore lint/style/noNonNullAssertion: The API for PutParameter states that there will always be a value returned when the request was successful. + return result.Version!; } /** @@ -832,25 +851,6 @@ class SSMProvider extends BaseProvider { return undefined; } - protected async _set( - name: string, - options: SSMSetOptions - ): Promise { - const sdkOptions: PutParameterCommandInput = { - Tier: options.tier ?? 'Standard', - Type: options.parameterType ?? 'String', - Overwrite: options.overwrite ?? false, - ...(options.kmsKeyId ? { KeyId: options.kmsKeyId } : {}), - ...(options.description ? { Description: options.description } : {}), - ...(options?.sdkOptions ?? {}), - Name: name, - Value: options.value, - }; - const result = await this.client.send(new PutParameterCommand(sdkOptions)); - - return result.Version; - } - /** * Split parameters that can be fetched by GetParameters vs GetParameter. * diff --git a/packages/parameters/src/ssm/setParameter.ts b/packages/parameters/src/ssm/setParameter.ts index 1bf7214c02..a1defc8081 100644 --- a/packages/parameters/src/ssm/setParameter.ts +++ b/packages/parameters/src/ssm/setParameter.ts @@ -97,7 +97,7 @@ const setParameter = async < >( name: string, options: InferredFromOptionsType & SSMSetOptions -): Promise => { +): Promise => { if (!Object.hasOwn(DEFAULT_PROVIDERS, 'ssm')) { DEFAULT_PROVIDERS.ssm = new SSMProvider(); } diff --git a/packages/parameters/tests/unit/SSMProvider.test.ts b/packages/parameters/tests/unit/SSMProvider.test.ts index e5bb5fd268..dfa2599258 100644 --- a/packages/parameters/tests/unit/SSMProvider.test.ts +++ b/packages/parameters/tests/unit/SSMProvider.test.ts @@ -1313,7 +1313,7 @@ describe('Class: SSMProvider', () => { }); }); - describe('Method: _set', () => { + describe('Method: set', () => { test('sets a parameter successfully', async () => { const provider: SSMProvider = new SSMProvider(); const client = mockClient(SSMClient) @@ -1331,17 +1331,6 @@ describe('Class: SSMProvider', () => { }); }); - test('returns undefined if version is undefined', async () => { - const provider: SSMProvider = new SSMProvider(); - mockClient(SSMClient).on(PutParameterCommand).resolves({}); - const parameterName: string = '/my-parameter'; - const options: SSMSetOptions = { value: 'my-value' }; - - const version = await provider.set(parameterName, options); - - expect(version).toBeUndefined(); - }); - test('sets a parameter with sdk options successfully', async () => { const provider: SSMProvider = new SSMProvider(); const client = mockClient(SSMClient) @@ -1372,7 +1361,7 @@ describe('Class: SSMProvider', () => { const options: SSMSetOptions = { value: 'my-value' }; await expect(provider.set(parameterName, options)).rejects.toThrow( - 'Failed to set parameter' + `Unable to set parameter with name ${parameterName}` ); }); diff --git a/packages/parameters/tests/unit/setParameter.test.ts b/packages/parameters/tests/unit/setParameter.test.ts index 3936a6b143..1e59f2a7c4 100644 --- a/packages/parameters/tests/unit/setParameter.test.ts +++ b/packages/parameters/tests/unit/setParameter.test.ts @@ -6,26 +6,32 @@ import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; import { mockClient } from 'aws-sdk-client-mock'; import { DEFAULT_PROVIDERS } from '../../src/base'; +import { setParameter } from '../../src/ssm'; import { SSMProvider } from '../../src/ssm/SSMProvider'; -import { setParameter } from '../../src/ssm/setParameter'; import 'aws-sdk-client-mock-jest'; import type { SSMSetOptions } from '../../src/types/SSMProvider'; describe('Function: setParameter', () => { const parameterName = '/my-parameter'; + const client = mockClient(SSMClient); beforeEach(() => { jest.clearAllMocks(); }); + afterEach(() => { + client.reset(); + }); + test('when called and a default provider does not exist, it instantiates one and sets the parameter', async () => { + // Prepare const options: SSMSetOptions = { value: 'my-value' }; - const client = mockClient(SSMClient) - .on(PutParameterCommand) - .resolves({ Version: 1 }); + client.on(PutParameterCommand).resolves({ Version: 1 }); + // Act const version = await setParameter(parameterName, options); + // Assess expect(client).toReceiveCommandWith(PutParameterCommand, { Name: parameterName, Value: options.value, @@ -34,15 +40,16 @@ describe('Function: setParameter', () => { }); test('when called and a default provider exists, it uses it and sets the parameter', async () => { + // Prepare const provider = new SSMProvider(); DEFAULT_PROVIDERS.ssm = provider; const options: SSMSetOptions = { value: 'my-value' }; - const client = mockClient(SSMClient) - .on(PutParameterCommand) - .resolves({ Version: 1 }); + client.on(PutParameterCommand).resolves({ Version: 1 }); + // Act const version = await setParameter(parameterName, options); + // Assess expect(client).toReceiveCommandWith(PutParameterCommand, { Name: parameterName, Value: options.value, @@ -51,30 +58,31 @@ describe('Function: setParameter', () => { expect(DEFAULT_PROVIDERS.ssm).toBe(provider); }); - test('when called and setting a parameter returns an undefined version, it returns undefined', async () => { + test('when called and the sdk client throws an error a custom error should be thrown from the function', async () => { + // Prepare const options: SSMSetOptions = { value: 'my-value' }; - const client = mockClient(SSMClient).on(PutParameterCommand).resolves({}); - - const version = await setParameter(parameterName, options); - - expect(client).toReceiveCommandWith(PutParameterCommand, { - Name: parameterName, - Value: options.value, - }); - expect(version).toBeUndefined(); + client.on(PutParameterCommand).rejects(new Error('Could not send command')); + + // Assess + expect(async () => { + await setParameter(parameterName, options); + }).rejects.toThrowError( + `Unable to set parameter with name ${parameterName}` + ); }); test('when called with additional sdk options, it sets the parameter with the sdk options successfully', async () => { + // Prepare const options: SSMSetOptions = { value: 'my-value', sdkOptions: { Overwrite: true }, }; - const client = mockClient(SSMClient) - .on(PutParameterCommand) - .resolves({ Version: 1 }); + client.on(PutParameterCommand).resolves({ Version: 1 }); + // Act const version = await setParameter(parameterName, options); + // Assess expect(client).toReceiveCommandWith(PutParameterCommand, { Name: parameterName, Value: options.value, @@ -92,13 +100,14 @@ describe('Function: setParameter', () => { ])( 'when called with %s option, it sets the parameter with the option successfully', async (option, value, sdkOption) => { + //Prepare const options: SSMSetOptions = { value: 'my-value', [option]: value }; - const client = mockClient(SSMClient) - .on(PutParameterCommand) - .resolves({ Version: 1 }); + client.on(PutParameterCommand).resolves({ Version: 1 }); + // Act const version = await setParameter(parameterName, options); + // Assess expect(client).toReceiveCommandWith(PutParameterCommand, { Name: parameterName, Value: options.value, From 642dd1cff66363454aface3013427bb0f4e756e7 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 10 Sep 2024 19:48:47 +0200 Subject: [PATCH 07/10] Update packages/parameters/src/ssm/setParameter.ts --- packages/parameters/src/ssm/setParameter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parameters/src/ssm/setParameter.ts b/packages/parameters/src/ssm/setParameter.ts index a1defc8081..847ca3d9b7 100644 --- a/packages/parameters/src/ssm/setParameter.ts +++ b/packages/parameters/src/ssm/setParameter.ts @@ -1,6 +1,6 @@ import { DEFAULT_PROVIDERS } from '../base/DefaultProviders.js'; import type { SSMSetOptions } from '../types/SSMProvider'; -import { SSMProvider } from './SSMProvider'; +import { SSMProvider } from './SSMProvider.js'; /** * ## Intro From 562ecb22e7c0234bae10552fb0edcea1168b1c84 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 10 Sep 2024 19:49:07 +0200 Subject: [PATCH 08/10] Update packages/parameters/src/ssm/setParameter.ts --- packages/parameters/src/ssm/setParameter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parameters/src/ssm/setParameter.ts b/packages/parameters/src/ssm/setParameter.ts index 847ca3d9b7..08d64b4c24 100644 --- a/packages/parameters/src/ssm/setParameter.ts +++ b/packages/parameters/src/ssm/setParameter.ts @@ -1,5 +1,5 @@ import { DEFAULT_PROVIDERS } from '../base/DefaultProviders.js'; -import type { SSMSetOptions } from '../types/SSMProvider'; +import type { SSMSetOptions } from '../types/SSMProvider.js'; import { SSMProvider } from './SSMProvider.js'; /** From 9d803d3ba11f257979430909fe6a33a96b794357 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 10 Sep 2024 19:50:15 +0200 Subject: [PATCH 09/10] Update packages/parameters/tests/unit/setParameter.test.ts --- packages/parameters/tests/unit/setParameter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parameters/tests/unit/setParameter.test.ts b/packages/parameters/tests/unit/setParameter.test.ts index 1e59f2a7c4..d52d0fb066 100644 --- a/packages/parameters/tests/unit/setParameter.test.ts +++ b/packages/parameters/tests/unit/setParameter.test.ts @@ -6,7 +6,7 @@ import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; import { mockClient } from 'aws-sdk-client-mock'; import { DEFAULT_PROVIDERS } from '../../src/base'; -import { setParameter } from '../../src/ssm'; +import { setParameter } from '../../src/ssm/index.js'; import { SSMProvider } from '../../src/ssm/SSMProvider'; import 'aws-sdk-client-mock-jest'; import type { SSMSetOptions } from '../../src/types/SSMProvider'; From 11cb54c5e17fbcf8fc1e1f06ec62bbb59807a129 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 10 Sep 2024 19:51:04 +0200 Subject: [PATCH 10/10] Apply suggestions from code review --- packages/parameters/tests/unit/setParameter.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/parameters/tests/unit/setParameter.test.ts b/packages/parameters/tests/unit/setParameter.test.ts index d52d0fb066..a7c6ad68af 100644 --- a/packages/parameters/tests/unit/setParameter.test.ts +++ b/packages/parameters/tests/unit/setParameter.test.ts @@ -5,11 +5,11 @@ */ import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; import { mockClient } from 'aws-sdk-client-mock'; -import { DEFAULT_PROVIDERS } from '../../src/base'; +import { DEFAULT_PROVIDERS } from '../../src/base/index.js'; import { setParameter } from '../../src/ssm/index.js'; -import { SSMProvider } from '../../src/ssm/SSMProvider'; +import { SSMProvider } from '../../src/ssm/SSMProvider.js'; import 'aws-sdk-client-mock-jest'; -import type { SSMSetOptions } from '../../src/types/SSMProvider'; +import type { SSMSetOptions } from '../../src/types/SSMProvider.js'; describe('Function: setParameter', () => { const parameterName = '/my-parameter';