Skip to content

improv(parser): allow extending base provider with no AWS SDK client #3564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/utilities/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ All caching logic is handled by the `BaseProvider`, and provided that the return

Here's an example of implementing a custom parameter store using an external service like HashiCorp Vault, a widely popular key-value secret storage.

=== "Provider usage"
```typescript hl_lines="5-8 12-16"
--8<-- "examples/snippets/parameters/customProviderVaultUsage.ts"
```

=== "Provider implementation"
```typescript
--8<-- "examples/snippets/parameters/customProviderVault.ts"
Expand All @@ -307,11 +312,6 @@ Here's an example of implementing a custom parameter store using an external ser
--8<-- "examples/snippets/parameters/customProviderVaultTypes.ts"
```

=== "Provider usage"
```typescript
--8<-- "examples/snippets/parameters/customProviderVaultUsage.ts"
```

### Deserializing values with transform parameter

For parameters stored in JSON or Base64 format, you can use the `transform` argument for deserialization.
Expand Down
23 changes: 7 additions & 16 deletions examples/snippets/parameters/customProviderVault.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Logger } from '@aws-lambda-powertools/logger';
import { BaseProvider } from '@aws-lambda-powertools/parameters/base';
import { GetParameterError } from '@aws-lambda-powertools/parameters/errors';
import Vault from 'hashi-vault-js';
import type {
HashiCorpVaultGetOptions,
Expand All @@ -9,17 +9,14 @@ import type {
class HashiCorpVaultProvider extends BaseProvider {
public client: Vault;
readonly #token: string;
readonly #logger: Logger;

/**
* It initializes the HashiCorpVaultProvider class.
*
* @param {HashiCorpVaultProviderOptions} config - The configuration object.
*/
public constructor(config: HashiCorpVaultProviderOptions) {
super({
proto: Vault,
});
super({});

const { url, token, clientConfig, vaultClient } = config;
if (vaultClient) {
Expand All @@ -39,9 +36,6 @@ class HashiCorpVaultProvider extends BaseProvider {
this.client = new Vault(config);
}
this.#token = token;
this.#logger = new Logger({
serviceName: 'HashiCorpVaultProvider',
});
}

/**
Expand All @@ -55,13 +49,13 @@ class HashiCorpVaultProvider extends BaseProvider {
* @param {string} name - The name of the secret
* @param {HashiCorpVaultGetOptions} options - Options to customize the retrieval of the secret
*/
public async get(
public async get<T extends Record<string, unknown>>(
name: string,
options?: HashiCorpVaultGetOptions
): Promise<Record<string, unknown> | undefined> {
): Promise<T | undefined> {
return super.get(name, options) as Promise<
Record<string, unknown> | undefined
>;
> as Promise<T | undefined>;
}

/**
Expand Down Expand Up @@ -92,9 +86,6 @@ class HashiCorpVaultProvider extends BaseProvider {
);

if (response.isVaultError) {
this.#logger.error('An error occurred', {
error: response.vaultHelpMessage,
});
throw response;
}
return response.data;
Expand All @@ -108,8 +99,8 @@ class HashiCorpVaultProvider extends BaseProvider {
protected async _getMultiple(
_path: string,
_options?: unknown
): Promise<void> {
throw new Error('Method not implemented.');
): Promise<Record<string, unknown> | undefined> {
throw new GetParameterError('Method not implemented.');
}
}

Expand Down
23 changes: 18 additions & 5 deletions examples/snippets/parameters/customProviderVaultUsage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { Logger } from '@aws-lambda-powertools/logger';
import { HashiCorpVaultProvider } from './customProviderVault.js';

const logger = new Logger({ logLevel: 'DEBUG' });
const secretsProvider = new HashiCorpVaultProvider({
url: 'https://vault.example.com:8200/v1',
token: 'my-token',
token: process.env.ROOT_TOKEN ?? '',
});

export const handler = async (): Promise<void> => {
try {
// Retrieve a secret from HashiCorp Vault
const secret = await secretsProvider.get('my-secret');
console.log(secret);
};
const secret = await secretsProvider.get<{ foo: 'string' }>('my-secret', {
sdkOptions: {
mount: 'secrets',
},
});
if (!secret) {
throw new Error('Secret not found');
}
logger.debug('Secret retrieved!');
} catch (error) {
if (error instanceof Error) {
logger.error(error.message, error);
}
}
2 changes: 1 addition & 1 deletion packages/parameters/src/appconfig/AppConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class AppConfigProvider extends BaseProvider {
*/
public constructor(options: AppConfigProviderOptions) {
super({
proto: AppConfigDataClient as new (
awsSdkV3ClientPrototype: AppConfigDataClient as new (
config?: unknown
) => AppConfigDataClient,
clientConfig: options.clientConfig,
Expand Down
21 changes: 10 additions & 11 deletions packages/parameters/src/base/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { EnvironmentVariablesService } from '../config/EnvironmentVariablesService.js';
import { GetParameterError, TransformParameterError } from '../errors.js';
import type {
BaseProviderConstructorOptions,
BaseProviderInterface,
GetMultipleOptionsInterface,
GetOptionsInterface,
Expand Down Expand Up @@ -44,27 +45,25 @@ abstract class BaseProvider implements BaseProviderInterface {
public constructor({
awsSdkV3Client,
clientConfig,
proto,
}: {
awsSdkV3Client?: unknown;
clientConfig?: unknown;
proto: new (config?: unknown) => unknown;
}) {
awsSdkV3ClientPrototype,
}: BaseProviderConstructorOptions) {
this.store = new Map();
this.envVarsService = new EnvironmentVariablesService();
if (awsSdkV3Client) {
if (!isSdkClient(awsSdkV3Client)) {
if (!isSdkClient(awsSdkV3Client) && awsSdkV3ClientPrototype) {
console.warn(
'awsSdkV3Client is not an AWS SDK v3 client, using default client'
);
this.client = new proto(clientConfig ?? {});
this.client = new awsSdkV3ClientPrototype(clientConfig ?? {});
} else {
this.client = awsSdkV3Client;
}
} else {
this.client = new proto(clientConfig ?? {});
} else if (awsSdkV3ClientPrototype) {
this.client = new awsSdkV3ClientPrototype(clientConfig ?? {});
}
if (isSdkClient(this.client)) {
addUserAgentMiddleware(this.client, 'parameters');
}
addUserAgentMiddleware(this.client, 'parameters');
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/parameters/src/dynamodb/DynamoDBProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ class DynamoDBProvider extends BaseProvider {
*/
public constructor(config: DynamoDBProviderOptions) {
super({
proto: DynamoDBClient as new (config?: unknown) => DynamoDBClient,
awsSdkV3ClientPrototype: DynamoDBClient as new (
config?: unknown
) => DynamoDBClient,
...config,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/parameters/src/secrets/SecretsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class SecretsProvider extends BaseProvider {
*/
public constructor(config?: SecretsProviderOptions) {
super({
proto: SecretsManagerClient as new (
awsSdkV3ClientPrototype: SecretsManagerClient as new (
config?: unknown
) => SecretsManagerClient,
...(config ?? {}),
Expand Down
2 changes: 1 addition & 1 deletion packages/parameters/src/ssm/SSMProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class SSMProvider extends BaseProvider {
*/
public constructor(config?: SSMProviderOptions) {
super({
proto: SSMClient as new (config?: unknown) => SSMClient,
awsSdkV3ClientPrototype: SSMClient as new (config?: unknown) => SSMClient,
...(config ?? {}),
});
}
Expand Down
23 changes: 23 additions & 0 deletions packages/parameters/src/types/BaseProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import type { Transform } from '../constants.js';

/**
* Options for the BaseProvider class constructor.
*/
type BaseProviderConstructorOptions = {
/**
* AWS SDK v3 client instance to use for operations.
*/
awsSdkV3Client?: unknown;
/**
* Optional configuration to pass during client initialization to customize AWS SDK v3 clients.
*/
clientConfig?: unknown;
/**
* AWS SDK v3 client prototype.
*
* If the `awsSdkV3Client` is not provided, this will be used to create a new client.
*/
awsSdkV3ClientPrototype: new (
config?: unknown
) => unknown;
};

/**
* Type for the transform option.
*/
Expand Down Expand Up @@ -79,6 +101,7 @@ interface BaseProviderInterface {
}

export type {
BaseProviderConstructorOptions,
GetOptionsInterface,
GetMultipleOptionsInterface,
BaseProviderInterface,
Expand Down