Skip to content

Commit 10bfb9e

Browse files
authored
feat: #400 Rplt 400 dns records (#11521)
* feat: initial idea setup * feat: added migrations for missing columns * fix: remove verifyName column * feat: added deployment service dns endpoints * feat: started adding dns frontend components * feat: added call for dns verify * fix: resolved annoying 'bucket already exists' issue * feat: commiting this so I don't lose it * feat: experimenting with dns creationst status * feat: added steps for dns setup frontend * feat: added eventbridge cross account trigger * feat: added poc for cross region and account event bridge setup * feat: added ui for custom dns * feat: added fetch methods for dns info * feat: added poc for managing dns * fix: corrected tests for pipeline funcs * feat: design changes on dns frontend * feat: navigate dns record setup * fix: remove unused var * feat: updated snapshots * feat: added dns provider tests * feat: correct error message display * fix: remove comment blocks * chore: linting * feat: added tests for dns controller * feat: updated ts-definitions * fix: omit unwanted types * feat: missing snapshots * feat: updated vite-pwa package * fix: improve complexity * feat: remove wildcard from certificate * feat: updated regex for reapit domains
1 parent 5a8112d commit 10bfb9e

File tree

130 files changed

+3231
-184
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+3231
-184
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
23.2 KB
Binary file not shown.
23.9 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

packages/deployment-service/README.md

+1-1

packages/deployment-service/cdk/lib/cdk-stack.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import * as cdk from 'aws-cdk-lib'
3030
import { ResolveProductionS3BucketPermissionsCustomResource } from './resolve-production-S3-bucket-permissions-custom-resource'
3131
import { ResolveProductionOACCustomResource } from './resolve-production-OAC-custom-resource'
3232
import { ResolveProductionS3BucketPoliciesCustomResource } from './resolve-production-S3-bucket-policies-custom-resource'
33+
import { DnsCertificateUpdate } from './dns-certificate-update'
3334

3435
export const databaseName = 'deployment_service'
3536

@@ -134,7 +135,12 @@ export const createStack = async () => {
134135
},
135136
appEvents: {
136137
handler: createFileLoc('sqs', 'handle'),
137-
policies: [...policies.commonBackendPolicies, policies.cloudFrontPolicy, policies.route53Policy],
138+
policies: [
139+
...policies.commonBackendPolicies,
140+
policies.cloudFrontPolicy,
141+
policies.route53Policy,
142+
policies.certificatePolicy,
143+
],
138144
queues: [queues[QueueNames.APP_EVENTS]],
139145
entrypoint: 'bundle/sqs.zip',
140146
},
@@ -250,6 +256,13 @@ export const createStack = async () => {
250256
}
251257
}
252258

259+
new DnsCertificateUpdate(stack, 'dns-certificate', {
260+
vpc,
261+
usercodeStack,
262+
environmentVars: env,
263+
policies: [...policies.commonBackendPolicies, policies.certificatePolicy, policies.cloudFrontPolicy],
264+
})
265+
253266
/**
254267
* NOTE: In order to make a successful deployment, migrations must be removed for the first deloy
255268
* thereafter, the migration script can be added

packages/deployment-service/cdk/lib/create-policies.ts

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum PolicyNames {
1414
secretManagerPolicy = 'secretManagerPolicy',
1515
S3BucketPolicy = 'S3BucketPolicy',
1616
originAccessControlPolicy = 'originAccessControlPolicy',
17+
certificatePolicy = 'certificatePolicy',
1718
}
1819

1920
type namedPolicyType = {
@@ -172,6 +173,12 @@ export const createPolicies = ({
172173
resources: ['*'],
173174
})
174175

176+
const certificatePolicy = new PolicyStatement({
177+
actions: ['acm:RequestCertificate', 'acm:AddTagsToCertificate', 'acm:DescribeCertificate', 'acm:DeleteCertificate'],
178+
effect: Effect.ALLOW,
179+
resources: ['*'],
180+
})
181+
175182
// create a policy that allows the lambda to do what it needs to do in the usercode stack
176183
const usercodePolicy = new Policy(usercodeStack, 'UsercodePolicy')
177184
usercodePolicy.addStatements(
@@ -182,6 +189,7 @@ export const createPolicies = ({
182189
codebuildExecPolicy,
183190
parameterStorePolicy,
184191
originAccessControlPolicy,
192+
certificatePolicy,
185193
)
186194
const usercodeStackRoleName = `${usercodeStack.stackName}-UsercodeStackRole`
187195
// create a role that lambdas can assume in the usercode stack, with the policy we just created
@@ -203,6 +211,7 @@ export const createPolicies = ({
203211
actions: ['sts:AssumeRole'],
204212
})
205213

214+
// TODO remove
206215
const lambdaInvoke = new PolicyStatement({
207216
effect: Effect.ALLOW,
208217
resources: [
@@ -231,5 +240,6 @@ export const createPolicies = ({
231240
secretManagerPolicy,
232241
S3BucketPolicy,
233242
usercodeStackRoleArn,
243+
certificatePolicy,
234244
}
235245
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { PolicyStatement } from '@reapit/ts-scripts/src/cdk'
2+
import { aws_ec2, aws_events, aws_events_targets, aws_iam, aws_lambda, Duration, Stack } from 'aws-cdk-lib'
3+
import { Construct } from 'constructs'
4+
5+
interface DnsCertificateUpdateOptionsInterface {
6+
usercodeStack: Stack
7+
vpc: aws_ec2.Vpc
8+
environmentVars: { [s: string]: string }
9+
policies: PolicyStatement[]
10+
}
11+
12+
export class DnsCertificateUpdate extends Construct {
13+
constructor(
14+
paasEuWest2Stack: Stack,
15+
id: string,
16+
{ usercodeStack: usercodeEuWest2Stack, vpc, environmentVars, policies }: DnsCertificateUpdateOptionsInterface,
17+
) {
18+
super(paasEuWest2Stack, id)
19+
20+
const userCodeUsEast1Stack = new Stack(usercodeEuWest2Stack, 'event-bus-us-east-1-usercode-stack', {
21+
env: { account: usercodeEuWest2Stack.account, region: 'us-east-1' },
22+
})
23+
24+
const eventBusUsEast1 = aws_events.EventBus.fromEventBusName(
25+
userCodeUsEast1Stack,
26+
`${id}-lookup-default-bus`,
27+
'default',
28+
)
29+
30+
const paasEventBridgeEuWest2 = new aws_events.EventBus(paasEuWest2Stack, `${id}-paas-event-bridge-acm`, {
31+
eventBusName: 'acm-bus',
32+
})
33+
34+
new aws_events.Rule(userCodeUsEast1Stack, `${id}-cross-account-rule`, {
35+
targets: [new aws_events_targets.EventBus(paasEventBridgeEuWest2)],
36+
eventBus: eventBusUsEast1,
37+
eventPattern: {
38+
source: ['aws.acm'],
39+
},
40+
})
41+
42+
paasEventBridgeEuWest2.addToResourcePolicy(
43+
new aws_iam.PolicyStatement({
44+
sid: 'AllowTrustedAccountToPutEvents',
45+
effect: aws_iam.Effect.ALLOW,
46+
actions: ['events:PutEvents'],
47+
resources: [paasEventBridgeEuWest2.eventBusArn],
48+
principals: [new aws_iam.AccountPrincipal(userCodeUsEast1Stack.account)],
49+
}),
50+
)
51+
52+
const certificateUpdateLambda = new aws_lambda.Function(paasEuWest2Stack, `${id}-eventbridge-update-lambda`, {
53+
runtime: aws_lambda.Runtime.NODEJS_18_X,
54+
handler: 'packages/deployment-service/dist/dns-eventbridge.handle',
55+
// code: aws_lambda.Code.fromInline(`
56+
// exports.handler = async (event) => {
57+
// console.log('Event received:', JSON.stringify(event));
58+
// return { statusCode: 200, body: 'Event processed' };
59+
// };
60+
// `),
61+
vpc,
62+
code: aws_lambda.Code.fromAsset('bundle/dns-eventbridge.zip'),
63+
timeout: Duration.seconds(30),
64+
memorySize: 512,
65+
environment: environmentVars,
66+
})
67+
68+
policies.forEach((policy) => certificateUpdateLambda.addToRolePolicy(policy))
69+
70+
new aws_events.Rule(paasEuWest2Stack, `${id}-test-trigger-rule`, {
71+
targets: [new aws_events_targets.LambdaFunction(certificateUpdateLambda)],
72+
eventBus: paasEventBridgeEuWest2,
73+
eventPattern: {
74+
source: ['aws.acm'],
75+
},
76+
})
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm'
2+
3+
export class AddDomainFields1729084248683 implements MigrationInterface {
4+
name = 'AddDomainFields1729084248683'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query('ALTER TABLE `pipelines` ADD `verifyDnsValue` varchar(255) NULL')
8+
await queryRunner.query('ALTER TABLE `pipelines` ADD `verifyDnsName` varchar(255) NULL')
9+
await queryRunner.query('ALTER TABLE `pipelines` ADD `customDomain` varchar(255) NULL')
10+
await queryRunner.query('ALTER TABLE `pipelines` ADD `domainVerified` tinyint NOT NULL')
11+
await queryRunner.query('ALTER TABLE `pipelines` ADD `certificateArn` varchar(255) NULL')
12+
}
13+
14+
public async down(queryRunner: QueryRunner): Promise<void> {
15+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `verifyDnsValue`')
16+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `verifyDnsName`')
17+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `customDomain`')
18+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `domainVerified`')
19+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `certificateArn`')
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm'
2+
3+
export class AddDomainStatusField1732024866737 implements MigrationInterface {
4+
name = 'AddDomainStatusField1732024866737'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(
8+
'ALTER TABLE `pipelines` ADD `certificateStatus` varchar(255) NOT NULL DEFAULT "unverified"',
9+
)
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query('ALTER TABLE `pipelines` DROP COLUMN `certificateStatus`')
14+
}
15+
}

packages/deployment-service/migrations/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { pipelineBitbucketRelation1654693666828 } from './1654693666828-pipeline
1919
import { pipelineRepositoryNullable1655394853865 } from './1655394853865-pipelineRepositoryNullable'
2020
import { githubRepository1697724871099 } from './1697724871099-github-repository'
2121
import { githubRepositoryDeveloperId1698161392543 } from './1698161392543-github-repository-developerId'
22+
import { AddDomainFields1729084248683 } from './1729084248683-AddDomainFields'
23+
import { AddDomainStatusField1732024866737 } from './1732024866737-AddDomainStatusField'
2224

2325
export default [
2426
pipelines1625758770110,
@@ -42,4 +44,6 @@ export default [
4244
pipelineRepositoryNullable1655394853865,
4345
githubRepository1697724871099,
4446
githubRepositoryDeveloperId1698161392543,
47+
AddDomainFields1729084248683,
48+
AddDomainStatusField1732024866737,
4549
]

packages/deployment-service/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "deployment-service",
33
"dependencies": {
4+
"@aws-sdk/client-acm": "^3.670.0",
45
"@aws-sdk/client-cloudfront": "^3.675.0",
56
"@aws-sdk/client-rds": "^3.709.0",
67
"@aws-sdk/client-route-53": "^3.675.0",
@@ -13,8 +14,8 @@
1314
"@nestjs/passport": "^11.0.4",
1415
"@nestjs/platform-express": "^11.0.5",
1516
"@nestjs/typeorm": "^8.1.4",
16-
"@octokit/app": "^14.1.0",
17-
"@reapit/foundations-ts-definitions": "^1.5.9",
17+
"@octokit/app": "^14.0.2",
18+
"@reapit/foundations-ts-definitions": "^2.1.20",
1819
"@reapit/utils-authorizer": "workspace:^",
1920
"@reapit/utils-nest": "workspace:packages/utils-nest",
2021
"@reapit/utils-node": "workspace:^",

packages/deployment-service/src/app-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { AwsModule } from './aws'
1515
import { CodeBuildModule } from './codebuild'
1616
import { APP_INTERCEPTOR } from '@nestjs/core'
1717
import { CorsHeaderInterceptor, AuthModule } from '@reapit/utils-nest'
18+
import { DnsModule } from './dns'
1819

1920
process.env = {
2021
...process.env,
@@ -52,6 +53,7 @@ process.env = {
5253
BitbucketModule,
5354
AwsModule,
5455
CodeBuildModule,
56+
DnsModule,
5557
],
5658
providers: [
5759
CorsHeaderInterceptor,

packages/deployment-service/src/aws/module.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Module } from '@nestjs/common'
55
import { ConfigModule, ConfigService } from '@nestjs/config'
66
import { CodeBuild, Credentials, S3, SSM, SQS } from 'aws-sdk'
77
import { Route53Client } from '@aws-sdk/client-route-53'
8+
import { ACMClient } from '@aws-sdk/client-acm'
89

910
export const ROLE_CREDENTIALS = 'ROLE_CREDENTIALS'
1011

@@ -63,7 +64,16 @@ export const ROLE_CREDENTIALS = 'ROLE_CREDENTIALS'
6364
provide: SQS,
6465
useFactory: () => new SQS({ apiVersion: '2012-11-05', endpoint: process.env.SQS_ENDPOINT }),
6566
},
67+
{
68+
provide: ACMClient,
69+
useFactory: (credentials) =>
70+
new ACMClient({
71+
region: 'us-east-1',
72+
credentials,
73+
}),
74+
inject: [ROLE_CREDENTIALS],
75+
},
6676
],
67-
exports: [S3, CodeBuild, SSM, ROLE_CREDENTIALS, CloudFrontClient, Route53Client, SQS],
77+
exports: [S3, CodeBuild, SSM, ROLE_CREDENTIALS, CloudFrontClient, Route53Client, SQS, ACMClient],
6878
})
6979
export class AwsModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NestFactory } from '@nestjs/core'
2+
import { AppModule } from './app-module'
3+
import { EventBridgeHandler } from 'aws-lambda'
4+
import { INestMicroservice } from '@nestjs/common'
5+
import { DnsEventBridgeProvider } from './dns/dns.eventbridge.provider'
6+
7+
let app: INestMicroservice
8+
9+
export type CertificateDetail = {
10+
CertificateType: string
11+
CommonName: string
12+
DomainValidationMethod: string
13+
Action: string
14+
DaysToExpiry: number
15+
CertificateCreatedDate: string
16+
CertificateExpirationDate: string
17+
InUse: false
18+
Exported: false
19+
}
20+
21+
const initApp = async (): Promise<INestMicroservice> => {
22+
const app = await NestFactory.createMicroservice<INestMicroservice>(AppModule)
23+
await app.init()
24+
25+
return app
26+
}
27+
28+
export const handle: EventBridgeHandler<'ACM Certificate Available', CertificateDetail, any> = async (event) => {
29+
app = app || (await initApp())
30+
31+
console.log('event', JSON.stringify(event))
32+
33+
const dnsEventBridgeProvider = app.get<DnsEventBridgeProvider>(DnsEventBridgeProvider)
34+
35+
await dnsEventBridgeProvider.handle(event)
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
ACMClient,
3+
CertificateDetail,
4+
DescribeCertificateCommand,
5+
RequestCertificateCommand,
6+
ResourceNotFoundException,
7+
} from '@aws-sdk/client-acm'
8+
import { PipelineEntity } from './../entities/pipeline.entity'
9+
import { Inject } from '@nestjs/common'
10+
11+
export class CertificateProvider {
12+
constructor(
13+
@Inject(ACMClient)
14+
private readonly client: ACMClient,
15+
) {}
16+
17+
async createCertificate(pipeline: PipelineEntity): Promise<string> {
18+
const result = await this.client.send(
19+
new RequestCertificateCommand({
20+
DomainName: pipeline.customDomain,
21+
ValidationMethod: 'DNS',
22+
SubjectAlternativeNames: [
23+
// `www.${pipeline.customDomain}`, // if www is required?
24+
pipeline.customDomain as string,
25+
],
26+
DomainValidationOptions: [
27+
{
28+
DomainName: pipeline.customDomain,
29+
ValidationDomain: pipeline.customDomain,
30+
},
31+
],
32+
Options: {
33+
CertificateTransparencyLoggingPreference: 'ENABLED',
34+
},
35+
Tags: [
36+
{
37+
Key: 'pipelineId',
38+
Value: pipeline.id,
39+
},
40+
{
41+
Key: 'onwer',
42+
Value: 'IaaS',
43+
},
44+
],
45+
}),
46+
)
47+
48+
return result.CertificateArn as string
49+
}
50+
51+
async obtainCertificate(pipeline: PipelineEntity): Promise<CertificateDetail | undefined> {
52+
if (!pipeline.certificateArn) return undefined
53+
54+
try {
55+
const certificate = await this.client.send(
56+
new DescribeCertificateCommand({
57+
CertificateArn: pipeline.certificateArn,
58+
}),
59+
)
60+
61+
return certificate.Certificate
62+
} catch (error: any) {
63+
if (error instanceof ResourceNotFoundException) {
64+
console.error(error.message)
65+
return undefined
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)