-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add ACM certificate construct #390
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 8 commits
a4e07cc
bc9c64f
b2a733d
9f9af45
60cd5ba
b4ce885
0327f64
89e9c5d
17a65a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`The GuCertificate class should create a new certificate (which requires manual DNS changes) if hosted zone ids are not provided 1`] = ` | ||
Object { | ||
"Mappings": Object { | ||
"stagemapping": Object { | ||
"CODE": Object { | ||
"domainName": "code-guardian.com", | ||
}, | ||
"PROD": Object { | ||
"domainName": "prod-guardian.com", | ||
}, | ||
}, | ||
}, | ||
"Parameters": Object { | ||
"Stage": Object { | ||
"AllowedValues": Array [ | ||
"CODE", | ||
"PROD", | ||
], | ||
"Default": "CODE", | ||
"Description": "Stage name", | ||
"Type": "String", | ||
}, | ||
}, | ||
"Resources": Object { | ||
"TestCertificate6B4956B6": Object { | ||
"DeletionPolicy": "Retain", | ||
"Properties": Object { | ||
"DomainName": Object { | ||
"Fn::FindInMap": Array [ | ||
"stagemapping", | ||
Object { | ||
"Ref": "Stage", | ||
}, | ||
"domainName", | ||
], | ||
}, | ||
"Tags": Array [ | ||
Object { | ||
"Key": "App", | ||
"Value": "testing", | ||
}, | ||
Object { | ||
"Key": "gu:cdk:version", | ||
"Value": "TEST", | ||
}, | ||
Object { | ||
"Key": "Stack", | ||
"Value": "test-stack", | ||
}, | ||
Object { | ||
"Key": "Stage", | ||
"Value": Object { | ||
"Ref": "Stage", | ||
}, | ||
}, | ||
], | ||
"ValidationMethod": "DNS", | ||
}, | ||
"Type": "AWS::CertificateManager::Certificate", | ||
"UpdateReplacePolicy": "Retain", | ||
}, | ||
}, | ||
} | ||
`; | ||
|
||
exports[`The GuCertificate class should create a new certificate when hosted zone ids are provided 1`] = ` | ||
Object { | ||
"Mappings": Object { | ||
"stagemapping": Object { | ||
"CODE": Object { | ||
"domainName": "code-guardian.com", | ||
"hostedZoneId": "id123", | ||
}, | ||
"PROD": Object { | ||
"domainName": "prod-guardian.com", | ||
"hostedZoneId": "id124", | ||
}, | ||
}, | ||
}, | ||
"Parameters": Object { | ||
"Stage": Object { | ||
"AllowedValues": Array [ | ||
"CODE", | ||
"PROD", | ||
], | ||
"Default": "CODE", | ||
"Description": "Stage name", | ||
"Type": "String", | ||
}, | ||
}, | ||
"Resources": Object { | ||
"TestCertificate6B4956B6": Object { | ||
"DeletionPolicy": "Retain", | ||
"Properties": Object { | ||
"DomainName": Object { | ||
"Fn::FindInMap": Array [ | ||
"stagemapping", | ||
Object { | ||
"Ref": "Stage", | ||
}, | ||
"domainName", | ||
], | ||
}, | ||
"DomainValidationOptions": Array [ | ||
Object { | ||
"DomainName": Object { | ||
"Fn::FindInMap": Array [ | ||
"stagemapping", | ||
Object { | ||
"Ref": "Stage", | ||
}, | ||
"domainName", | ||
], | ||
}, | ||
"HostedZoneId": Object { | ||
"Fn::FindInMap": Array [ | ||
"stagemapping", | ||
Object { | ||
"Ref": "Stage", | ||
}, | ||
"hostedZoneId", | ||
], | ||
}, | ||
}, | ||
], | ||
"Tags": Array [ | ||
Object { | ||
"Key": "App", | ||
"Value": "testing", | ||
}, | ||
Object { | ||
"Key": "gu:cdk:version", | ||
"Value": "TEST", | ||
}, | ||
Object { | ||
"Key": "Stack", | ||
"Value": "test-stack", | ||
}, | ||
Object { | ||
"Key": "Stage", | ||
"Value": Object { | ||
"Ref": "Stage", | ||
}, | ||
}, | ||
], | ||
"ValidationMethod": "DNS", | ||
}, | ||
"Type": "AWS::CertificateManager::Certificate", | ||
"UpdateReplacePolicy": "Retain", | ||
}, | ||
}, | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { SynthUtils } from "@aws-cdk/assert"; | ||
import { Stage } from "../../constants"; | ||
import type { SynthedStack } from "../../utils/test"; | ||
import { simpleGuStackForTesting } from "../../utils/test"; | ||
import { GuCertificate } from "./certificate"; | ||
|
||
describe("The GuCertificate class", () => { | ||
it("should create a new certificate when hosted zone ids are provided", () => { | ||
const stack = simpleGuStackForTesting(); | ||
new GuCertificate(stack, "TestCertificate", { | ||
app: "testing", | ||
[Stage.CODE]: { | ||
domainName: "code-guardian.com", | ||
hostedZoneId: "id123", | ||
}, | ||
[Stage.PROD]: { | ||
domainName: "prod-guardian.com", | ||
hostedZoneId: "id124", | ||
}, | ||
}); | ||
expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); | ||
}); | ||
|
||
it("should create a new certificate (which requires manual DNS changes) if hosted zone ids are not provided", () => { | ||
const stack = simpleGuStackForTesting(); | ||
new GuCertificate(stack, "TestCertificate", { | ||
app: "testing", | ||
[Stage.CODE]: { | ||
domainName: "code-guardian.com", | ||
}, | ||
[Stage.PROD]: { | ||
domainName: "prod-guardian.com", | ||
}, | ||
}); | ||
expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); | ||
}); | ||
|
||
it("should inherit a CloudFormed certificate correctly", () => { | ||
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); | ||
new GuCertificate(stack, "TestCertificate", { | ||
app: "testing", | ||
existingLogicalId: "MyCloudFormedCertificate", | ||
[Stage.CODE]: { | ||
domainName: "code-guardian.com", | ||
hostedZoneId: "id123", | ||
}, | ||
[Stage.PROD]: { | ||
domainName: "prod-guardian.com", | ||
hostedZoneId: "id124", | ||
}, | ||
}); | ||
const json = SynthUtils.toCloudFormation(stack) as SynthedStack; | ||
expect(Object.keys(json.Resources)).toContain("MyCloudFormedCertificate"); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { Certificate, CertificateValidation } from "@aws-cdk/aws-certificatemanager"; | ||
import type { CertificateProps } from "@aws-cdk/aws-certificatemanager/lib/certificate"; | ||
import { HostedZone } from "@aws-cdk/aws-route53"; | ||
import { RemovalPolicy } from "@aws-cdk/core"; | ||
import { Stage } from "../../constants"; | ||
import type { GuStack } from "../core"; | ||
import { AppIdentity } from "../core/identity"; | ||
import { GuMigratingResource } from "../core/migrating"; | ||
import type { GuStatefulConstruct } from "../core/migrating"; | ||
|
||
export type GuCertificateProps = Record<Stage, GuDnsValidatedCertificateProps> & GuMigratingResource & AppIdentity; | ||
|
||
export interface GuDnsValidatedCertificateProps { | ||
domainName: string; | ||
hostedZoneId?: string; | ||
} | ||
|
||
/** | ||
* Construct which creates an ACM Certificate. | ||
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. minor: Should we make it explicit that this is a DNS validated cert? Either by mentioning it in this comment or in the naming of the construct? 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 idea, I've updated the docs. |
||
* | ||
* If your DNS is managed via Route 53, then supplying `hostedZoneId` props will allow AWS to automatically | ||
* validate your certificate. | ||
* | ||
* If your DNS is not managed via Route 53, or you omit the `hostedZoneId` props, then the CloudFormation | ||
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. This advice is based on https://github.com/aws/aws-cdk/blob/7966f8d48c4bff26beb22856d289f9d0c7e7081d/packages/%40aws-cdk/aws-certificatemanager/lib/certificate.ts#L101-L116. In the future, perhaps we can use a custom resource to create the appropriate records elsewhere (NS1?) automatically? |
||
* operation which adds this construct will pause until the relevant DNS record has been added manually. | ||
* | ||
* Example usage for creating a new certificate: | ||
* | ||
* ```typescript | ||
* new GuCertificate(stack, "TestCertificate", { | ||
* app: "testing", | ||
* [Stage.CODE]: { | ||
* domainName: "code-guardian.com", | ||
* hostedZoneId: "id123", | ||
* }, | ||
* [Stage.PROD]: { | ||
* domainName: "prod-guardian.com", | ||
* hostedZoneId: "id124", | ||
* }, | ||
* }); | ||
*``` | ||
* | ||
* Example usage for inheriting a certificate which was created via CloudFormation: | ||
* | ||
* ```typescript | ||
* new GuCertificate(stack, "TestCertificate", { | ||
* app: "testing", | ||
* existingLogicalId: "MyCloudFormedCertificate", | ||
* [Stage.CODE]: { | ||
* domainName: "code-guardian.com", | ||
* hostedZoneId: "id123", | ||
* }, | ||
* [Stage.PROD]: { | ||
* domainName: "prod-guardian.com", | ||
* hostedZoneId: "id124", | ||
* }, | ||
* }); | ||
*``` | ||
*/ | ||
export class GuCertificate extends Certificate implements GuStatefulConstruct { | ||
isStatefulConstruct: true; | ||
constructor(scope: GuStack, id: string, props: GuCertificateProps) { | ||
const maybeHostedZone = | ||
props.CODE.hostedZoneId && props.PROD.hostedZoneId | ||
? HostedZone.fromHostedZoneId( | ||
scope, | ||
"HostedZone", | ||
scope.withStageDependentValue({ | ||
variableName: "hostedZoneId", | ||
stageValues: { [Stage.CODE]: props.CODE.hostedZoneId, [Stage.PROD]: props.PROD.hostedZoneId }, | ||
}) | ||
) | ||
: undefined; | ||
const awsCertificateProps: CertificateProps = { | ||
domainName: scope.withStageDependentValue({ | ||
variableName: "domainName", | ||
stageValues: { [Stage.CODE]: props.CODE.domainName, [Stage.PROD]: props.PROD.domainName }, | ||
}), | ||
validation: CertificateValidation.fromDns(maybeHostedZone), | ||
}; | ||
super(scope, id, awsCertificateProps); | ||
this.applyRemovalPolicy(RemovalPolicy.RETAIN); | ||
this.isStatefulConstruct = true; | ||
GuMigratingResource.setLogicalId(this, scope, { existingLogicalId: props.existingLogicalId }); | ||
AppIdentity.taggedConstruct({ app: props.app }, this); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./certificate"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that adding this deletion policy will improve safety and allow us to import certificates created outside of CloudFormation 🤞