-
Notifications
You must be signed in to change notification settings - Fork 156
feat(parameters): SecretsProvider support #1206
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
Changes from 4 commits
d0a3d23
e778fdd
ae9bac4
bbfa196
31a493f
2e3c5b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { BaseProvider } from '../BaseProvider'; | ||
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; | ||
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; | ||
import type { SecretsProviderOptions, SecretsGetOptionsInterface } from 'types/SecretsProvider'; | ||
|
||
class SecretsProvider extends BaseProvider { | ||
public client: SecretsManagerClient; | ||
|
||
public constructor (config?: SecretsProviderOptions) { | ||
super(); | ||
|
||
const clientConfig = config?.clientConfig || {}; | ||
this.client = new SecretsManagerClient(clientConfig); | ||
} | ||
|
||
public async get(name: string, options?: SecretsGetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> { | ||
return super.get(name, options); | ||
} | ||
|
||
protected async _get(name: string, options?: SecretsGetOptionsInterface): Promise<string | Uint8Array | undefined> { | ||
const sdkOptions: GetSecretValueCommandInput = { | ||
SecretId: name, | ||
}; | ||
if (options?.sdkOptions) { | ||
this.removeNonOverridableOptions(options.sdkOptions as GetSecretValueCommandInput); | ||
Object.assign(sdkOptions, options.sdkOptions); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I looked at the comment above the
It may be less readable, but I like it more than duplicating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have applied your suggestion. Thank you! |
||
|
||
const result = await this.client.send(new GetSecretValueCommand(sdkOptions)); | ||
|
||
if (result.SecretString) return result.SecretString; | ||
|
||
return result.SecretBinary; | ||
} | ||
|
||
/** | ||
* Retrieving multiple parameter values is not supported with AWS Secrets Manager. | ||
*/ | ||
protected async _getMultiple(_path: string, _options?: unknown): Promise<Record<string, string | undefined>> { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
/** | ||
* Explicit arguments passed to the constructor will take precedence over ones passed to the method. | ||
* For users who consume the library with TypeScript, this will be enforced by the type system. However, | ||
* for JavaScript users, we need to manually delete the properties that are not allowed to be overridden. | ||
*/ | ||
protected removeNonOverridableOptions(options: GetSecretValueCommandInput): void { | ||
if (options.hasOwnProperty('SecretId')) { | ||
delete options.SecretId; | ||
} | ||
} | ||
} | ||
|
||
export { | ||
SecretsProvider, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { DEFAULT_PROVIDERS } from '../BaseProvider'; | ||
import { SecretsProvider } from './SecretsProvider'; | ||
import type { SecretsGetOptionsInterface } from '../types/SecretsProvider'; | ||
|
||
const getSecret = async (name: string, options?: SecretsGetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> => { | ||
if (!DEFAULT_PROVIDERS.hasOwnProperty('secrets')) { | ||
DEFAULT_PROVIDERS.secrets = new SecretsProvider(); | ||
} | ||
|
||
return DEFAULT_PROVIDERS.secrets.get(name, options); | ||
}; | ||
|
||
export { | ||
getSecret | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './SecretsProvider'; | ||
export * from './getSecret'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { GetOptionsInterface } from './BaseProvider'; | ||
import type { SecretsManagerClientConfig, GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; | ||
|
||
interface SecretsProviderOptions { | ||
clientConfig?: SecretsManagerClientConfig | ||
} | ||
|
||
interface SecretsGetOptionsInterface extends GetOptionsInterface { | ||
sdkOptions?: Omit<Partial<GetSecretValueCommandInput>, 'SecretId'> | ||
} | ||
|
||
export type { | ||
SecretsProviderOptions, | ||
SecretsGetOptionsInterface, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/** | ||
* Test SecretsProvider class | ||
* | ||
* @group unit/parameters/SecretsProvider/class | ||
*/ | ||
import { SecretsProvider } from '../../src/secrets'; | ||
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; | ||
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager'; | ||
import { mockClient } from 'aws-sdk-client-mock'; | ||
import 'aws-sdk-client-mock-jest'; | ||
|
||
const encoder = new TextEncoder(); | ||
|
||
describe('Class: SecretsProvider', () => { | ||
|
||
const client = mockClient(SecretsManagerClient); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe('Method: _get', () => { | ||
|
||
test('when called with only a name, it gets the secret string', async () => { | ||
|
||
// Prepare | ||
const provider = new SecretsProvider(); | ||
const secretName = 'foo'; | ||
client.on(GetSecretValueCommand).resolves({ | ||
SecretString: 'bar', | ||
}); | ||
|
||
// Act | ||
const result = await provider.get(secretName); | ||
|
||
// Assess | ||
expect(result).toBe('bar'); | ||
|
||
}); | ||
|
||
test('when called with only a name, it gets the secret binary', async () => { | ||
|
||
// Prepare | ||
const provider = new SecretsProvider(); | ||
const secretName = 'foo'; | ||
const mockData = encoder.encode('my-value'); | ||
client.on(GetSecretValueCommand).resolves({ | ||
SecretBinary: mockData, | ||
}); | ||
|
||
// Act | ||
const result = await provider.get(secretName); | ||
|
||
// Assess | ||
expect(result).toBe(mockData); | ||
|
||
}); | ||
|
||
test('when called with a name and sdkOptions, it gets the secret using the options provided', async () => { | ||
|
||
// Prepare | ||
const provider = new SecretsProvider(); | ||
const secretName = 'foo'; | ||
client.on(GetSecretValueCommand).resolves({ | ||
SecretString: 'bar', | ||
}); | ||
|
||
// Act | ||
await provider.get(secretName, { | ||
sdkOptions: { | ||
VersionId: 'test-version', | ||
} | ||
}); | ||
|
||
// Assess | ||
expect(client).toReceiveCommandWith(GetSecretValueCommand, { | ||
SecretId: secretName, | ||
VersionId: 'test-version', | ||
}); | ||
|
||
}); | ||
|
||
}); | ||
|
||
describe('Method: _getMultiple', () => { | ||
|
||
test('when called, it throws an error', async () => { | ||
|
||
// Prepare | ||
const provider = new SecretsProvider(); | ||
|
||
// Act & Assess | ||
await expect(provider.getMultiple('foo')).rejects.toThrow('Method not implemented.'); | ||
|
||
}); | ||
|
||
}); | ||
|
||
describe('Method: removeNonOverridableOptions', () => { | ||
|
||
class DummyProvider extends SecretsProvider { | ||
public removeNonOverridableOptions(options: GetSecretValueCommandInput): void { | ||
super.removeNonOverridableOptions(options); | ||
} | ||
} | ||
|
||
test('when called with a valid GetSecretValueCommandInput, it removes the non-overridable options', () => { | ||
|
||
// Prepare | ||
const provider = new DummyProvider(); | ||
const options: GetSecretValueCommandInput = { | ||
SecretId: 'test-secret', | ||
VersionId: 'test-version', | ||
}; | ||
|
||
// Act | ||
provider.removeNonOverridableOptions(options); | ||
|
||
// Assess | ||
expect(options).toEqual({ | ||
VersionId: 'test-version', | ||
}); | ||
|
||
}); | ||
|
||
}); | ||
|
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* Test getSecret function | ||
* | ||
* @group unit/parameters/SecretsProvider/getSecret/function | ||
*/ | ||
import { DEFAULT_PROVIDERS } from '../../src/BaseProvider'; | ||
import { SecretsProvider, getSecret } from '../../src/secrets'; | ||
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; | ||
import { mockClient } from 'aws-sdk-client-mock'; | ||
import 'aws-sdk-client-mock-jest'; | ||
|
||
const encoder = new TextEncoder(); | ||
|
||
describe('Function: getSecret', () => { | ||
|
||
const client = mockClient(SecretsManagerClient); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test('when called and a default provider doesn\'t exist, it instantiates one and returns the value', async () => { | ||
|
||
// Prepare | ||
const secretName = 'foo'; | ||
const secretValue = 'bar'; | ||
client.on(GetSecretValueCommand).resolves({ | ||
SecretString: secretValue, | ||
}); | ||
|
||
// Act | ||
const result = await getSecret(secretName); | ||
|
||
// Assess | ||
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName }); | ||
expect(result).toBe(secretValue); | ||
|
||
}); | ||
|
||
test('when called and a default provider exists, it uses it and returns the value', async () => { | ||
|
||
// Prepare | ||
const provider = new SecretsProvider(); | ||
DEFAULT_PROVIDERS.secrets = provider; | ||
const secretName = 'foo'; | ||
const secretValue = 'bar'; | ||
const binary = encoder.encode(secretValue); | ||
client.on(GetSecretValueCommand).resolves({ | ||
SecretBinary: binary, | ||
}); | ||
|
||
// Act | ||
const result = await getSecret(secretName); | ||
|
||
// Assess | ||
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName }); | ||
expect(result).toStrictEqual(binary); | ||
expect(DEFAULT_PROVIDERS.secrets).toBe(provider); | ||
|
||
}); | ||
|
||
}); |
Uh oh!
There was an error while loading. Please reload this page.