Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions src/v2/components/acm-certificate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import { commonTags } from '../../../constants';

export namespace AcmCertificate {
export type Args = {
domain: pulumi.Input<string>;
hostedZoneId: pulumi.Input<string>;
};
}

export class AcmCertificate extends pulumi.ComponentResource {
certificate: aws.acm.Certificate;

constructor(
name: string,
args: AcmCertificate.Args,
opts: pulumi.ComponentResourceOptions = {},
) {
super('studion:acm:Certificate', name, {}, opts);

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],
},
{ parent: this },
);

this.registerOutputs();
}
}
1 change: 1 addition & 0 deletions src/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { WebServerLoadBalancer } from './components/web-server/load-balancer';
export { ElastiCacheRedis } from './components/redis/elasticache-redis';
export { UpstashRedis } from './components/redis/upstash-redis';
export { Vpc } from './components/vpc';
export { AcmCertificate } from './components/acm-certificate';
export { Password } from './components/password';

import { OtelCollectorBuilder } from './otel/builder';
Expand Down
118 changes: 118 additions & 0 deletions tests/acm-certificate/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as assert from 'node:assert';
import * as automation from '../automation';
import { InlineProgramArgs } from '@pulumi/pulumi/automation';
import { ACMClient } from '@aws-sdk/client-acm';
import { Route53Client } from '@aws-sdk/client-route-53';
import { backOff } from 'exponential-backoff';
import {
DescribeCertificateCommand,
CertificateType,
} from '@aws-sdk/client-acm';
import { ListResourceRecordSetsCommand } from '@aws-sdk/client-route-53';
import { AcmCertificateTestContext } from './test-context';
import { describe, it, before, after } from 'node:test';
import { requireEnv } from '../util';

const programArgs: InlineProgramArgs = {
stackName: 'dev',
projectName: 'icb-test-acm-certificate',
program: () => import('./infrastructure'),
};

const region = requireEnv('AWS_REGION');
const domainName = requireEnv('ICB_DOMAIN_NAME');
const hostedZoneId = requireEnv('ICB_HOSTED_ZONE_ID');

const ctx: AcmCertificateTestContext = {
outputs: {},
config: {
exponentialBackOffConfig: {
delayFirstAttempt: true,
numOfAttempts: 5,
startingDelay: 2000,
timeMultiple: 1.5,
jitter: 'full',
},
},
clients: {
acm: new ACMClient({ region }),
route53: new Route53Client({ region }),
},
};

describe('ACM Certificate component deployment', () => {
before(async () => {
ctx.outputs = await automation.deploy(programArgs);
});

after(() => automation.destroy(programArgs));

it('should create certificate with correct domain name', async () => {
const certificate = ctx.outputs.certificate.value;
assert.ok(certificate.certificate, 'Should have certificate property');
assert.ok(certificate.certificate.arn, 'Certificate should have ARN');

return backOff(async () => {
const certResult = await ctx.clients.acm.send(
new DescribeCertificateCommand({
CertificateArn: certificate.certificate.arn,
}),
);

const cert = certResult.Certificate;
assert.ok(cert, 'Certificate should exist');
assert.strictEqual(
cert.DomainName,
domainName,
'Certificate domain should match',
);
assert.strictEqual(
cert.Type,
CertificateType.AMAZON_ISSUED,
'Should be Amazon issued certificate',
);
}, ctx.config.exponentialBackOffConfig);
});

it('should have validation record with correct resource record value', async () => {
const certificate = ctx.outputs.certificate.value;
const hostedZone = ctx.outputs.hostedZone.value;

const certResult = await ctx.clients.acm.send(
new DescribeCertificateCommand({
CertificateArn: certificate.certificate.arn,
}),
);

const domainValidation =
certResult.Certificate?.DomainValidationOptions?.[0];
assert.ok(domainValidation, 'Should have domain validation options');
assert.ok(
domainValidation.ResourceRecord,
'Validation resource record should exists',
);

const recordsResult = await ctx.clients.route53.send(
new ListResourceRecordSetsCommand({
HostedZoneId: hostedZone.zoneId,
}),
);

const records = recordsResult.ResourceRecordSets || [];
const validationRecord = records.find(
record => record.Name === domainValidation.ResourceRecord?.Name,
);

assert.ok(validationRecord, 'Validation record should exist');
assert.strictEqual(
validationRecord.TTL,
600,
'Validation record should have 600 TTL',
);
assert.strictEqual(
validationRecord.ResourceRecords?.[0]?.Value,
domainValidation.ResourceRecord?.Value,
'Validation record should have correct value',
);
});
});
16 changes: 16 additions & 0 deletions tests/acm-certificate/infrastructure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { next as studion } from '@studion/infra-code-blocks';
import * as aws from '@pulumi/aws';

const appName = 'acm-certificate-test';

const hostedZone = aws.route53.getZoneOutput({
zoneId: process.env.ICB_HOSTED_ZONE_ID,
privateZone: false,
});

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

export { certificate, hostedZone };
33 changes: 33 additions & 0 deletions tests/acm-certificate/test-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { OutputMap } from '@pulumi/pulumi/automation';
import { ACMClient } from '@aws-sdk/client-acm';
import { Route53Client } from '@aws-sdk/client-route-53';

interface AcmCertificateTestConfig {
exponentialBackOffConfig: {
delayFirstAttempt: boolean;
numOfAttempts: number;
startingDelay: number;
timeMultiple: number;
jitter: 'full' | 'none';
};
}

interface ConfigContext {
config: AcmCertificateTestConfig;
}

interface PulumiProgramContext {
outputs: OutputMap;
}

interface AwsContext {
clients: {
acm: ACMClient;
route53: Route53Client;
};
}

export interface AcmCertificateTestContext
extends ConfigContext,
PulumiProgramContext,
AwsContext {}