Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
12bd0dd
Create acm certificate v2 component
bornast Dec 5, 2025
b8b8d94
Implement acm certificate tests
bornast Dec 5, 2025
431039c
Add legacy prefix to certificate v1 component
bornast Dec 5, 2025
6249fa5
Add subjectAlternativeNames option arg
bornast Dec 9, 2025
710a102
Add test for cert with SANs
bornast Dec 9, 2025
1c98b9e
Add acm certificate namespace for types
bornast Dec 10, 2025
06184a8
Remove legacy prefix from v1 certificate component
bornast Dec 10, 2025
50ff252
Rename test assertions
bornast Dec 10, 2025
5082c38
Fallback to hosted zone id arg if zone is not found by domain name
bornast Dec 10, 2025
266a103
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
46ca7e8
Fix get zone method args
bornast Dec 10, 2025
80e5f92
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
9a82628
Refactor get zone method args
bornast Dec 10, 2025
359a94b
Add ICB prefix to env variables
bornast Dec 10, 2025
2497cc5
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Dec 10, 2025
034c447
Fix method name typo
bornast Dec 10, 2025
e56724e
Export certificate using esmodule syntax
bornast Dec 16, 2025
5c53176
Merge branch task/certificate-v2-component into task/certificate-v2-s…
bornast Dec 16, 2025
fbf61a3
Merge branch 'master' into task/certificate-v2-component
bornast Jan 9, 2026
18fa3b6
Move setup to the top-level to prevent false positives
bornast Jan 9, 2026
ded476b
Merge branch 'task/certificate-v2-component' into task/certificate-v2…
bornast Jan 9, 2026
9471692
Merge branch 'master' into task/certificate-v2-san-arg
bornast Jan 12, 2026
9eb79b7
Use deepStrictEqual instead of for each when asserting
bornast Jan 12, 2026
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
69 changes: 44 additions & 25 deletions src/v2/components/acm-certificate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { commonTags } from '../../../constants';
export namespace AcmCertificate {
export type Args = {
domain: pulumi.Input<string>;
/**
* Additional domains/subdomains to be included in this certificate.
*/
subjectAlternativeNames?: pulumi.Input<string>[];
hostedZoneId: pulumi.Input<string>;
};
}
Expand All @@ -21,36 +25,51 @@ export class AcmCertificate extends pulumi.ComponentResource {

this.certificate = new aws.acm.Certificate(
`${args.domain}-certificate`,
{ domainName: args.domain, validationMethod: 'DNS', tags: commonTags },
{ parent: this },
);

const certificateValidationDomain = new aws.route53.Record(
`${args.domain}-cert-validation-domain`,
{
name: this.certificate.domainValidationOptions[0].resourceRecordName,
type: this.certificate.domainValidationOptions[0].resourceRecordType,
zoneId: args.hostedZoneId,
records: [
this.certificate.domainValidationOptions[0].resourceRecordValue,
],
ttl: 600,
},
{
parent: this,
deleteBeforeReplace: true,
},
);

const certificateValidation = new aws.acm.CertificateValidation(
`${args.domain}-cert-validation`,
{
certificateArn: this.certificate.arn,
validationRecordFqdns: [certificateValidationDomain.fqdn],
domainName: args.domain,
subjectAlternativeNames: args.subjectAlternativeNames,
validationMethod: 'DNS',
tags: commonTags,
},
{ parent: this },
);

this.createCertValidationRecords(args.domain, args.hostedZoneId);

this.registerOutputs();
}

private createCertValidationRecords(
domainName: AcmCertificate.Args['domain'],
hostedZoneId: AcmCertificate.Args['hostedZoneId'],
) {
this.certificate.domainValidationOptions.apply(domains => {
const validationRecords = domains.map(
domain =>
new aws.route53.Record(
`${domain.domainName}-cert-validation-domain`,
{
name: domain.resourceRecordName,
type: domain.resourceRecordType,
zoneId: hostedZoneId,
records: [domain.resourceRecordValue],
ttl: 600,
},
{
parent: this,
deleteBeforeReplace: true,
},
),
);

const certificateValidation = new aws.acm.CertificateValidation(
`${domainName}-cert-validation`,
{
certificateArn: this.certificate.arn,
validationRecordFqdns: validationRecords.map(record => record.fqdn),
},
{ parent: this },
);
});
}
}
24 changes: 24 additions & 0 deletions tests/acm-certificate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const hostedZoneId = requireEnv('ICB_HOSTED_ZONE_ID');
const ctx: AcmCertificateTestContext = {
outputs: {},
config: {
subDomainName: `app.${domainName}`,
exponentialBackOffConfig: {
delayFirstAttempt: true,
numOfAttempts: 5,
Expand Down Expand Up @@ -115,4 +116,27 @@ describe('ACM Certificate component deployment', () => {
'Validation record should have correct value',
);
});

it('should create certificate with subject alternative names', async () => {
const sanCertificate = ctx.outputs.sanCertificate.value;
const certResult = await ctx.clients.acm.send(
new DescribeCertificateCommand({
CertificateArn: sanCertificate.certificate.arn,
}),
);
const cert = certResult.Certificate;
const sans = new Set(cert?.SubjectAlternativeNames ?? []);

const expectedDomains = new Set([
ctx.config.subDomainName,
`api.${ctx.config.subDomainName}`,
`test.${ctx.config.subDomainName}`,
]);

assert.deepStrictEqual(
sans,
expectedDomains,
'Certificate should include all expected domains',
);
});
});
15 changes: 13 additions & 2 deletions tests/acm-certificate/infrastructure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ const hostedZone = aws.route53.getZoneOutput({
privateZone: false,
});

const domainName = process.env.ICB_DOMAIN_NAME!;
const certificate = new studion.AcmCertificate(`${appName}-certificate`, {
domain: process.env.ICB_DOMAIN_NAME!,
domain: domainName,
hostedZoneId: hostedZone.zoneId,
});

export { certificate, hostedZone };
const subDomainName = `app.${domainName}`;
const sanCertificate = new studion.AcmCertificate(
`${appName}-certificate-san`,
{
domain: subDomainName,
subjectAlternativeNames: [`api.${subDomainName}`, `test.${subDomainName}`],
hostedZoneId: hostedZone.zoneId,
},
);

export { certificate, sanCertificate, hostedZone };
1 change: 1 addition & 0 deletions tests/acm-certificate/test-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ACMClient } from '@aws-sdk/client-acm';
import { Route53Client } from '@aws-sdk/client-route-53';

interface AcmCertificateTestConfig {
subDomainName: string;
exponentialBackOffConfig: {
delayFirstAttempt: boolean;
numOfAttempts: number;
Expand Down