diff --git a/src/v2/components/acm-certificate/index.ts b/src/v2/components/acm-certificate/index.ts index 91f2005..76ed8c0 100644 --- a/src/v2/components/acm-certificate/index.ts +++ b/src/v2/components/acm-certificate/index.ts @@ -5,6 +5,10 @@ import { commonTags } from '../../../constants'; export namespace AcmCertificate { export type Args = { domain: pulumi.Input; + /** + * Additional domains/subdomains to be included in this certificate. + */ + subjectAlternativeNames?: pulumi.Input[]; hostedZoneId: pulumi.Input; }; } @@ -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 }, + ); + }); + } } diff --git a/tests/acm-certificate/index.test.ts b/tests/acm-certificate/index.test.ts index adbe963..7b458ce 100644 --- a/tests/acm-certificate/index.test.ts +++ b/tests/acm-certificate/index.test.ts @@ -31,6 +31,7 @@ describe('ACM Certificate component deployment', () => { const ctx: AcmCertificateTestContext = { outputs: {}, config: { + subDomainName: `app.${domainName}`, exponentialBackOffConfig: { delayFirstAttempt: true, numOfAttempts: 5, @@ -119,4 +120,28 @@ 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 = cert?.SubjectAlternativeNames || []; + + const expectedDomains = [ + ctx.config.subDomainName, + `api.${ctx.config.subDomainName}`, + `test.${ctx.config.subDomainName}`, + ]; + + expectedDomains.forEach(expectedDomain => { + assert.ok( + sans.includes(expectedDomain), + `Certificate should include: ${expectedDomain}`, + ); + }); + }); }); diff --git a/tests/acm-certificate/infrastructure/index.ts b/tests/acm-certificate/infrastructure/index.ts index 74802d9..c501aa6 100644 --- a/tests/acm-certificate/infrastructure/index.ts +++ b/tests/acm-certificate/infrastructure/index.ts @@ -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 }; diff --git a/tests/acm-certificate/test-context.ts b/tests/acm-certificate/test-context.ts index 69d0a55..7d4441a 100644 --- a/tests/acm-certificate/test-context.ts +++ b/tests/acm-certificate/test-context.ts @@ -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;