Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sl): enhance site launch process to utilise DNS indirection layer #920

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
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