From 8fd5479c6da2d60921df92fe7e5e72a0e03d5745 Mon Sep 17 00:00:00 2001 From: Joshua Weber <57131123+daschaa@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:11:51 +0200 Subject: [PATCH] feat(parameters): adds setParameter function to store SSM parameters (#3020) --- docs/utilities/parameters.md | 19 ++- examples/snippets/parameters/setParameter.ts | 7 ++ .../parameters/setParameterOverwrite.ts | 10 ++ packages/parameters/src/errors.ts | 12 +- packages/parameters/src/ssm/SSMProvider.ts | 63 +++++++++- packages/parameters/src/ssm/index.ts | 1 + packages/parameters/src/ssm/setParameter.ts | 107 ++++++++++++++++ packages/parameters/src/types/SSMProvider.ts | 40 ++++++ .../ssmProvider.class.test.functionCode.ts | 25 ++++ .../tests/e2e/ssmProvider.class.test.ts | 32 +++++ .../parameters/tests/unit/SSMProvider.test.ts | 79 ++++++++++++ .../tests/unit/setParameter.test.ts | 119 ++++++++++++++++++ 12 files changed, 511 insertions(+), 3 deletions(-) create mode 100644 examples/snippets/parameters/setParameter.ts create mode 100644 examples/snippets/parameters/setParameterOverwrite.ts create mode 100644 packages/parameters/src/ssm/setParameter.ts create mode 100644 packages/parameters/tests/unit/setParameter.test.ts diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index f0460eac4..4cbaf5364 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 000000000..d8de0004e --- /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 000000000..bb6f6f448 --- /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); +}; diff --git a/packages/parameters/src/errors.ts b/packages/parameters/src/errors.ts index fab94a770..5f5325c14 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 d1a1a5641..e3a041347 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,12 +11,14 @@ import type { GetParametersByPathCommandInput, 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, @@ -26,6 +29,7 @@ import type { SSMGetParametersByNameOutput, SSMGetParametersByNameOutputInterface, SSMProviderOptions, + SSMSetOptions, SSMSplitBatchAndDecryptParametersOutputType, } from '../types/SSMProvider.js'; @@ -322,6 +326,63 @@ 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 { + 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!; + } + /** * Retrieve multiple values from AWS Systems Manager. * diff --git a/packages/parameters/src/ssm/index.ts b/packages/parameters/src/ssm/index.ts index ffa9aba3a..a4459d0c1 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 000000000..08d64b4c2 --- /dev/null +++ b/packages/parameters/src/ssm/setParameter.ts @@ -0,0 +1,107 @@ +import { DEFAULT_PROVIDERS } from '../base/DefaultProviders.js'; +import type { SSMSetOptions } from '../types/SSMProvider.js'; +import { SSMProvider } from './SSMProvider.js'; + +/** + * ## 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 b647fe29c..889438db7 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/e2e/ssmProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts index 01429bd9a..6c6c6e13c 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 cea69b6df..bb5618a19 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/c', + } + ); + 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 () => { diff --git a/packages/parameters/tests/unit/SSMProvider.test.ts b/packages/parameters/tests/unit/SSMProvider.test.ts index ee9edc217..dfa259925 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,81 @@ 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('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(); + 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( + `Unable to set parameter with name ${parameterName}` + ); + }); + + 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 000000000..a7c6ad68a --- /dev/null +++ b/packages/parameters/tests/unit/setParameter.test.ts @@ -0,0 +1,119 @@ +/** + * 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/index.js'; +import { setParameter } from '../../src/ssm/index.js'; +import { SSMProvider } from '../../src/ssm/SSMProvider.js'; +import 'aws-sdk-client-mock-jest'; +import type { SSMSetOptions } from '../../src/types/SSMProvider.js'; + +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' }; + client.on(PutParameterCommand).resolves({ Version: 1 }); + + // Act + const version = await setParameter(parameterName, options); + + // Assess + 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 () => { + // Prepare + const provider = new SSMProvider(); + DEFAULT_PROVIDERS.ssm = provider; + const options: SSMSetOptions = { value: 'my-value' }; + client.on(PutParameterCommand).resolves({ Version: 1 }); + + // Act + const version = await setParameter(parameterName, options); + + // Assess + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + }); + expect(version).toBe(1); + expect(DEFAULT_PROVIDERS.ssm).toBe(provider); + }); + + 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' }; + 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 }, + }; + client.on(PutParameterCommand).resolves({ Version: 1 }); + + // Act + const version = await setParameter(parameterName, options); + + // Assess + 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) => { + //Prepare + const options: SSMSetOptions = { value: 'my-value', [option]: value }; + client.on(PutParameterCommand).resolves({ Version: 1 }); + + // Act + const version = await setParameter(parameterName, options); + + // Assess + expect(client).toReceiveCommandWith(PutParameterCommand, { + Name: parameterName, + Value: options.value, + [sdkOption]: value, + }); + expect(version).toBe(1); + } + ); +});