Skip to content

Commit

Permalink
feat(sl): enhance site launch process to utilise DNS indirection layer (
Browse files Browse the repository at this point in the history
#920)

* Add new configuration variables for DNS indirection layer

* Introduce new database column for indirection domain

* Insert intermediate step of creating indirection domain

* Switch env vars to constants instead

* Remove indirection domain database column
  • Loading branch information
dcshzj authored Aug 29, 2023
1 parent c3528a5 commit a00eb2e
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@ export const ISOMER_ADMIN_REPOS = [
export const INACTIVE_USER_THRESHOLD_DAYS = 60
export const GITHUB_ORG_REPOS_ENDPOINT = `https://api.github.com/orgs/${ISOMER_GITHUB_ORG_NAME}/repos`
export const REDIRECTION_SERVER_IP = config.get("redirectionServer.elasticIp")
export const DNS_INDIRECTION_DOMAIN = "hostedon.isomer.gov.sg"
export const DNS_INDIRECTION_REPO = "isomer-indirection"
export const ISOMER_ADMIN_EMAIL = "[email protected]"
export const ISOMER_SUPPORT_EMAIL = "[email protected]"
11 changes: 11 additions & 0 deletions src/errors/GitHubApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseIsomerError } from "./BaseError"

export default class GitHubApiError extends BaseIsomerError {
constructor(message: string) {
super({
status: 500,
code: "GitHubApiError",
message,
})
}
}
1 change: 1 addition & 0 deletions src/services/identity/LaunchesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type SiteLaunchCreateParams = {
domainValidationTarget: string
redirectionDomainSource?: string
redirectionDomainTarget?: string
indirectionDomain: string
}

interface LaunchesServiceProps {
Expand Down
111 changes: 111 additions & 0 deletions src/services/identity/ReposService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { Octokit } from "@octokit/rest"
import { GetResponseTypeFromEndpointMethod } from "@octokit/types"
import git from "isomorphic-git"
import http from "isomorphic-git/http/node"
import { ResultAsync, errAsync, okAsync } from "neverthrow"
import { ModelStatic } from "sequelize"

import { config } from "@config/config"

import { UnprocessableError } from "@errors/UnprocessableError"

import { Repo, Site } from "@database/models"
import { DNS_INDIRECTION_REPO } from "@root/constants"
import GitHubApiError from "@root/errors/GitHubApiError"
import logger from "@root/logger/logger"

const SYSTEM_GITHUB_TOKEN = config.get("github.systemToken")
const octokit = new Octokit({ auth: SYSTEM_GITHUB_TOKEN })
Expand Down Expand Up @@ -262,4 +266,111 @@ export default class ReposService {
remoteRef: "master",
})
}

createDnsIndirectionFile = (
indirectionSubdomain: string,
primaryDomain: string,
primaryDomainTarget: string
): ResultAsync<void, GitHubApiError> => {
const template = `import { Record } from "@pulumi/aws/route53";
import { CLOUDFRONT_HOSTED_ZONE_ID } from "../constants";
export const createRecords = (zoneId: string): Record[] => {
const records = [
new Record("${primaryDomain} A", {
name: "${indirectionSubdomain}",
type: "A",
zoneId: zoneId,
aliases: [
{
name: "${primaryDomainTarget}",
zoneId: CLOUDFRONT_HOSTED_ZONE_ID,
evaluateTargetHealth: false,
},
],
}),
new Record("${primaryDomain} AAAA", {
name: "${indirectionSubdomain}",
type: "AAAA",
zoneId: zoneId,
aliases: [
{
name: "${primaryDomainTarget}",
zoneId: CLOUDFRONT_HOSTED_ZONE_ID,
evaluateTargetHealth: false,
},
],
}),
];
return records;
};
`

return ResultAsync.fromPromise(
octokit.repos.getContent({
owner: ISOMER_GITHUB_ORGANIZATION_NAME,
repo: DNS_INDIRECTION_REPO,
path: `dns/${primaryDomain}.ts`,
}),
() => errAsync<true>(true)
)
.andThen((response) => {
if (Array.isArray(response.data)) {
logger.error(
`Error creating DNS indirection file for ${primaryDomain}`
)

return errAsync(
new GitHubApiError("Unable to create DNS indirection file")
)
}

const { sha } = response.data
return okAsync(sha)
})
.andThen((sha) =>
ResultAsync.fromPromise(
octokit.repos.createOrUpdateFileContents({
owner: ISOMER_GITHUB_ORGANIZATION_NAME,
repo: DNS_INDIRECTION_REPO,
path: `dns/${primaryDomain}.ts`,
message: `Update ${primaryDomain}.ts`,
content: Buffer.from(template).toString("base64"),
sha,
}),
(error) => {
logger.error(
`Error creating DNS indirection file for ${primaryDomain}: ${error}`
)

return new GitHubApiError("Unable to create DNS indirection file")
}
)
)
.orElse((error) => {
if (error instanceof GitHubApiError) {
return errAsync(error)
}

return ResultAsync.fromPromise(
octokit.repos.createOrUpdateFileContents({
owner: ISOMER_GITHUB_ORGANIZATION_NAME,
repo: DNS_INDIRECTION_REPO,
path: `dns/${primaryDomain}.ts`,
message: `Create ${primaryDomain}.ts`,
content: Buffer.from(template).toString("base64"),
}),
(error) => {
logger.error(
`Error creating DNS indirection file for ${primaryDomain}: ${error}`
)

return new GitHubApiError("Unable to create DNS indirection file")
}
)
})
.map(() => undefined)
}
}
39 changes: 39 additions & 0 deletions src/services/infra/InfraService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {
RedirectionTypes,
REDIRECTION_SERVER_IP,
ISOMER_SUPPORT_EMAIL,
DNS_INDIRECTION_DOMAIN,
} from "@root/constants"
import GitHubApiError from "@root/errors/GitHubApiError"
import MissingSiteError from "@root/errors/MissingSiteError"
import MissingUserEmailError from "@root/errors/MissingUserEmailError"
import SiteLaunchError from "@root/errors/SiteLaunchError"
Expand Down Expand Up @@ -207,6 +209,8 @@ export default class InfraService {
return url
}

convertDotsToDashes = (url: string) => url.replace(/\./g, "-")

isValidUrl(url: string): boolean {
const schema = Joi.string().domain()
// joi reports initial "_" for certificates as as an invalid url WRONGLY,
Expand Down Expand Up @@ -319,6 +323,22 @@ export default class InfraService {
})
}

getIndirectionDomain(
primaryDomain: string,
primaryDomainTarget: string
): ResultAsync<string, GitHubApiError> {
const indirectionSubdomain = this.convertDotsToDashes(primaryDomain)
const indirectionDomain = `${indirectionSubdomain}.${DNS_INDIRECTION_DOMAIN}`

return this.reposService
.createDnsIndirectionFile(
indirectionSubdomain,
primaryDomain,
primaryDomainTarget
)
.map(() => indirectionDomain)
}

getGeneratedDnsRecords = async (
siteName: string
): Promise<
Expand Down Expand Up @@ -486,6 +506,23 @@ export default class InfraService {
(subDomain) => subDomain.subDomainSetting?.prefix
)

// Indirection domain should look something like this:
// blah-gov-sg.hostedon.isomer.gov.sg
const indirectionDomain = await this.getIndirectionDomain(
primaryDomain,
primaryDomainTarget
)

if (indirectionDomain.isErr()) {
return errAsync(
new AmplifyError(
`Error creating indirection domain: ${indirectionDomain.error}`,
repoName,
appId
)
)
}

/**
* Amplify only stores the prefix.
* ie: if I wanted to have a www.blah.gov.sg -> gibberish.cloudfront.net,
Expand All @@ -500,6 +537,7 @@ export default class InfraService {
primaryDomainTarget,
domainValidationSource,
domainValidationTarget,
indirectionDomain: indirectionDomain.value,
}

if (redirectionDomainList?.length) {
Expand All @@ -520,6 +558,7 @@ export default class InfraService {
primaryDomainTarget,
domainValidationSource,
domainValidationTarget,
indirectionDomain: indirectionDomain.value,
requestorEmail: requestor.email ? requestor.email : "",
agencyEmail: agency.email ? agency.email : "", // TODO: remove conditional after making email not optional/nullable
}
Expand Down
1 change: 1 addition & 0 deletions src/services/infra/__tests__/DynamoDBService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const mockLaunch: SiteLaunchMessage = {
primaryDomainTarget: "myapp.example.com",
domainValidationSource: "example.com",
domainValidationTarget: "myapp.example.com",
indirectionDomain: "example.hostedon.example.com",
requestorEmail: "[email protected]",
agencyEmail: "[email protected]",
githubRedirectionUrl: "https://github.com/my-repo",
Expand Down
3 changes: 2 additions & 1 deletion src/services/utilServices/SendDNSRecordEmailClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface DnsRecordsEmailProps {
domainValidationTarget: string
primaryDomainSource: string
primaryDomainTarget: string
indirectionDomain: string
redirectionDomainSource?: string
redirectionDomainTarget?: string
quadARecords?: QuadARecord[]
Expand Down Expand Up @@ -87,7 +88,7 @@ export function getDNSRecordsEmailBody(
? `www.${dnsRecords.primaryDomainSource}`
: dnsRecords.primaryDomainSource
}</td>
<td style="${tdStyle}">${dnsRecords.primaryDomainTarget}</td>
<td style="${tdStyle}">${dnsRecords.indirectionDomain}</td>
<td style="${tdStyle}">CNAME</td>
</tr>`

Expand Down
2 changes: 2 additions & 0 deletions src/types/siteLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface SiteLaunchMessage {
primaryDomainTarget: string
domainValidationSource: string
domainValidationTarget: string
indirectionDomain: string
requestorEmail: string
agencyEmail: string
githubRedirectionUrl?: string
Expand Down Expand Up @@ -55,6 +56,7 @@ export function isSiteLaunchMessage(obj: unknown): obj is SiteLaunchMessage {
typeof message.primaryDomainTarget === "string" &&
typeof message.domainValidationSource === "string" &&
typeof message.domainValidationTarget === "string" &&
typeof message.indirectionDomain === "string" &&
typeof message.requestorEmail === "string" &&
typeof message.agencyEmail === "string" &&
(typeof message.githubRedirectionUrl === "undefined" ||
Expand Down

0 comments on commit a00eb2e

Please sign in to comment.