-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create GuSSMParameter using AWS Custom Resources (#336)
* Create GuSSMParameter and the AWS Custom Resource that allows it to fetch latest version * Create GuSSMParameter * remove extras * Add tests for random UIDs * update package-lock * feat: revise custom lint rule (#352) We have a few too many instances of // eslint-disable-next-line custom-rules/valid-constructors 😅 . It looks like this is mainly because the id field is static (for example the Stage parameter) or generated by the props passed in (for example InstanceType). This PR revises the rules to be: 1. Private constructors don't get linted 2. Must be 1, 2 or 3 parameters 3. First parameter must be called scope 4. First parameter must be of type GuStack 5. If 2 parameters: - The second parameter must be called props - The second parameter must be a custom type 6. If 3 parameters: - The second parameter must be called id - The second parameter must be of type string - The third parameter must be called props - The third parameter must be a custom type 7. Only the third parameter can be optional or have a default value * chore(release): 6.1.0 (#360) * extract id to fn outside constructor * Add tests for random UIDs Co-authored-by: stephengeller <[email protected]> Co-authored-by: Akash Askoolum <[email protected]> Co-authored-by: theguardian.com continuous integration <[email protected]>
- Loading branch information
1 parent
39934c6
commit b65a4f0
Showing
9 changed files
with
383 additions
and
2 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { GetParameterRequest } from "aws-sdk/clients/ssm"; | ||
|
||
export interface CustomResourceGetParameterProps { | ||
apiRequest: GetParameterRequest; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/* | ||
* The reason for this file is to appease the tests in `core/ssm.test.ts`. | ||
* We don't need this to contain anything useful because we're not testing the contents of it within that testing suite. | ||
* */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../runtime/lambda.ts |
17 changes: 17 additions & 0 deletions
17
src/constructs/core/custom-resources/runtime/lambda.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/* | ||
* You can't specify the file extension (eg .ts) of the file to import, | ||
* and lambda.js exists as a dummy file, so we'd always import that over lambda.ts (the actual code to be tested) if | ||
* we imported `lambda` | ||
* lambda.symlink is a symlink to lambda.ts, so we can test it without importing lambda.js | ||
* */ | ||
import { flatten } from "./lambda.symlink"; | ||
|
||
describe("The flatten function", function () { | ||
it("should flatten nested objects into one", function () { | ||
expect(flatten({ foo: { bar: "baz" } })).toEqual({ "foo.bar": "baz" }); | ||
}); | ||
|
||
it("should flatten infinitely nested objects into one", function () { | ||
expect(flatten({ foo: { bar: { baz: { blah: "foo" } } } })).toEqual({ "foo.bar.baz.blah": "foo" }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { request } from "https"; | ||
import { parse } from "url"; | ||
// eslint-disable-next-line import/no-unresolved -- this comes from @types/aws-lambda, but eslint can't seem to read it properly | ||
import type { CloudFormationCustomResourceEvent, Context } from "aws-lambda"; | ||
import SSM from "aws-sdk/clients/ssm"; | ||
import type { CustomResourceGetParameterProps } from "../interfaces"; | ||
|
||
/* eslint-disable -- This function is copied straight from AWS */ | ||
// https://github.com/aws/aws-cdk/blob/95438b56bfdc90e94f969f6998e5b5b680cbd7a8/packages/%40aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts#L16-L29 | ||
export function flatten(object: Record<string, unknown>): Record<string, string> { | ||
return Object.assign( | ||
{}, | ||
...(function _flatten(child: Record<string, any>, path: string[] = []): any { | ||
return [].concat( | ||
...Object.keys(child).map((key) => { | ||
const childKey = Buffer.isBuffer(child[key]) ? child[key].toString() : child[key]; | ||
return typeof childKey === "object" | ||
? _flatten(childKey, path.concat([key])) | ||
: { [path.concat([key]).join(".")]: childKey }; | ||
}) | ||
); | ||
})(object) | ||
) as Record<string, string>; | ||
} | ||
/* eslint-enable */ | ||
|
||
export async function handler(event: CloudFormationCustomResourceEvent, context: Context): Promise<void> { | ||
try { | ||
console.log(JSON.stringify(event)); | ||
|
||
// Default physical resource id | ||
let physicalResourceId: string; | ||
switch (event.RequestType) { | ||
case "Create": | ||
physicalResourceId = event.LogicalResourceId; | ||
break; | ||
case "Update": | ||
physicalResourceId = event.PhysicalResourceId; | ||
break; | ||
case "Delete": | ||
await respond("SUCCESS", "OK", event.PhysicalResourceId, {}); | ||
return; | ||
} | ||
|
||
let data: Record<string, string> = {}; | ||
|
||
const getParamsProps = event.ResourceProperties.getParamsProps as string | undefined; | ||
|
||
if (getParamsProps) { | ||
const request = decodeCall(getParamsProps); | ||
const ssmClient = new SSM(); | ||
const response = await ssmClient.getParameter(request.apiRequest).promise(); | ||
console.log("Response:", JSON.stringify(response, null, 4)); | ||
data = { ...flatten((response as unknown) as Record<string, string>) }; | ||
} | ||
|
||
console.log("data: ", data); | ||
await respond("SUCCESS", "OK", physicalResourceId, data); | ||
} catch (e) { | ||
console.log(e); | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- e.message is valid, see docs above | ||
await respond("FAILED", e.message || "Internal Error", context.logStreamName, {}); | ||
} | ||
|
||
function respond(responseStatus: string, reason: string, physicalResourceId: string, data: Record<string, string>) { | ||
const responseBody = JSON.stringify({ | ||
Status: responseStatus, | ||
Reason: reason, | ||
PhysicalResourceId: physicalResourceId, | ||
StackId: event.StackId, | ||
RequestId: event.RequestId, | ||
LogicalResourceId: event.LogicalResourceId, | ||
NoEcho: false, | ||
Data: data, | ||
}); | ||
|
||
console.log("Responding", responseBody); | ||
const parsedUrl = parse(event.ResponseURL); | ||
const requestOptions = { | ||
hostname: parsedUrl.hostname, | ||
path: parsedUrl.path, | ||
method: "PUT", | ||
headers: { "content-type": "", "content-length": responseBody.length }, | ||
}; | ||
|
||
return new Promise((resolve, reject) => { | ||
try { | ||
const r = request(requestOptions, resolve); | ||
r.on("error", reject); | ||
r.write(responseBody); | ||
r.end(); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
function decodeCall(call: string): CustomResourceGetParameterProps { | ||
return JSON.parse(call) as CustomResourceGetParameterProps; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import "@aws-cdk/assert/jest"; | ||
import { simpleGuStackForTesting } from "../../../test/utils"; | ||
import { GuSSMIdentityParameter, GuSSMParameter, id } from "./ssm"; | ||
|
||
describe("SSM:", () => { | ||
describe("The GuSSMIdentityParameter construct", () => { | ||
it("requires the scope and parameter name", function () { | ||
const stack = simpleGuStackForTesting({ stack: "some-stack" }); | ||
const param = new GuSSMIdentityParameter(stack, { parameter: "some-param", app: "foo" }); | ||
expect(stack).toHaveResourceLike("Custom::GuGetSSMParameter", { | ||
getParamsProps: { | ||
"Fn::Join": ["", ['{"apiRequest":{"Name":"/', { Ref: "Stage" }, '/some-stack/foo/some-param"}}']], | ||
}, | ||
}); | ||
expect(param.getValue()).toMatch(/TOKEN/i); | ||
}); | ||
}); | ||
|
||
describe("The GuSSMParameter construct", () => { | ||
it("requires the scope and parameter name", function () { | ||
const stack = simpleGuStackForTesting({ stack: "some-stack" }); | ||
const param = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); | ||
expect(stack).toHaveResourceLike("Custom::GuGetSSMParameter", { | ||
getParamsProps: '{"apiRequest":{"Name":"/path/to/some-param"}}', | ||
}); | ||
expect(param.getValue()).toMatch(/TOKEN/i); | ||
}); | ||
|
||
it("creates unique IDs using param and stack name for the parameters", function () { | ||
const stack = simpleGuStackForTesting({ stack: "some-stack" }); | ||
const param1 = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); | ||
const param2 = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); | ||
|
||
expect(param1.toString()).toMatch(/Test\/GuSSMParameter-pathtosomeparam/i); | ||
expect(param2.toString()).toMatch(/Test\/GuSSMParameter-pathtosomeparam/i); | ||
expect(param1.toString()).not.toEqual(param2.toString()); | ||
}); | ||
|
||
it("creates unique IDs that handles tokens", function () { | ||
const stack = simpleGuStackForTesting({ stack: "some-stack" }); | ||
const param1 = new GuSSMParameter(stack, { parameter: `/path/${stack.stage}/some-param` }); | ||
const param2 = new GuSSMParameter(stack, { parameter: `/path/${stack.stage}/some-param` }); | ||
expect(param1.toString()).toMatch(/Test\/GuSSMParameter-token/i); | ||
expect(param2.toString()).toMatch(/Test\/GuSSMParameter-token/i); | ||
expect(param1.toString()).not.toEqual(param2.toString()); | ||
}); | ||
}); | ||
|
||
describe("the id function", function () { | ||
it("creates a unique ID given a string", function () { | ||
expect(id("NameOfConstruct", "some-parameter")).toMatch(/NameOfConstruct-someparameter/i); | ||
}); | ||
|
||
it("will substitute a CDK token for an acceptable string", function () { | ||
expect(id("NameOfConstruct", "${TOKEN}foobar")).toMatch(/NameOfConstruct-token/i); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.