From 124b919a38fdff712af2a1914fa6360367b251a3 Mon Sep 17 00:00:00 2001 From: 0fatal <72899968+0fatal@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:46:57 +0800 Subject: [PATCH] feat(server): support setting custom domain for application (#1310) * feat(server): support setting custom domain for application * feat(server): support tls for app custom domain * feat(server): support gzip for apisix routes --- .../src/application/application.controller.ts | 85 ++++++- .../src/gateway/apisix-custom-cert.service.ts | 223 +++++++++++++++--- server/src/gateway/apisix.service.ts | 91 ++++++- server/src/gateway/entities/runtime-domain.ts | 5 +- .../gateway/runtime-domain-task.service.ts | 124 +++++++++- server/src/gateway/runtime-domain.service.ts | 63 +++++ 6 files changed, 535 insertions(+), 56 deletions(-) diff --git a/server/src/application/application.controller.ts b/server/src/application/application.controller.ts index c4c38a2247..4189763806 100644 --- a/server/src/application/application.controller.ts +++ b/server/src/application/application.controller.ts @@ -10,7 +10,12 @@ import { Post, Delete, } from '@nestjs/common' -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger' import { IRequest } from '../utils/interface' import { JwtAuthGuard } from '../authentication/jwt.auth.guard' import { @@ -41,6 +46,9 @@ import { Runtime } from './entities/runtime' import { ObjectId } from 'mongodb' import { ApplicationBundle } from './entities/application-bundle' import { ResourceService } from 'src/billing/resource.service' +import { RuntimeDomainService } from 'src/gateway/runtime-domain.service' +import { BindCustomDomainDto } from 'src/website/dto/update-website.dto' +import { RuntimeDomain } from 'src/gateway/entities/runtime-domain' @ApiTags('Application') @Controller('applications') @@ -55,6 +63,7 @@ export class ApplicationController { private readonly storage: StorageService, private readonly account: AccountService, private readonly resource: ResourceService, + private readonly runtimeDomain: RuntimeDomainService, ) {} /** @@ -320,6 +329,80 @@ export class ApplicationController { return ResponseUtil.ok(doc) } + /** + * Bind custom domain to application + */ + @ApiResponseObject(RuntimeDomain) + @ApiOperation({ summary: 'Bind custom domain to application' }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Patch(':appid/domain') + async bindDomain( + @Param('appid') appid: string, + @Body() dto: BindCustomDomainDto, + ) { + const runtimeDomain = await this.runtimeDomain.findOne(appid) + if ( + runtimeDomain?.customDomain && + runtimeDomain.customDomain === dto.domain + ) { + return ResponseUtil.error('domain already binded') + } + + // check if domain resolved + const resolved = await this.runtimeDomain.checkResolved(appid, dto.domain) + if (!resolved) { + return ResponseUtil.error('domain not resolved') + } + + // bind domain + const binded = await this.runtimeDomain.bindCustomDomain(appid, dto.domain) + if (!binded) { + return ResponseUtil.error('failed to bind domain') + } + + return ResponseUtil.ok(binded) + } + + /** + * Check if domain is resolved + */ + @ApiResponse({ type: ResponseUtil }) + @ApiOperation({ summary: 'Check if domain is resolved' }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Post(':appid/domain/resolved') + async checkResolved( + @Param('appid') appid: string, + @Body() dto: BindCustomDomainDto, + ) { + const resolved = await this.runtimeDomain.checkResolved(appid, dto.domain) + if (!resolved) { + return ResponseUtil.error('domain not resolved') + } + + return ResponseUtil.ok(resolved) + } + + /** + * Remove custom domain of application + */ + @ApiResponseObject(RuntimeDomain) + @ApiOperation({ summary: 'Remove custom domain of application' }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Delete(':appid/domain') + async remove(@Param('appid') appid: string) { + const runtimeDomain = await this.runtimeDomain.findOne(appid) + if (!runtimeDomain?.customDomain) { + return ResponseUtil.error('custom domain not found') + } + + const deleted = await this.runtimeDomain.removeCustomDomain(appid) + if (!deleted) { + return ResponseUtil.error('failed to remove custom domain') + } + + return ResponseUtil.ok(deleted) + } + /** * Delete an application */ diff --git a/server/src/gateway/apisix-custom-cert.service.ts b/server/src/gateway/apisix-custom-cert.service.ts index 921b511daf..b5976ae735 100644 --- a/server/src/gateway/apisix-custom-cert.service.ts +++ b/server/src/gateway/apisix-custom-cert.service.ts @@ -4,6 +4,7 @@ import { ClusterService } from 'src/region/cluster/cluster.service' import { Region } from 'src/region/entities/region' import { GetApplicationNamespaceByAppId } from 'src/utils/getter' import { WebsiteHosting } from 'src/website/entities/website' +import { RuntimeDomain } from './entities/runtime-domain' // This class handles the creation and deletion of website domain certificates // and ApisixTls resources using Kubernetes Custom Resource Definitions (CRDs). @@ -12,11 +13,10 @@ export class ApisixCustomCertService { private readonly logger = new Logger(ApisixCustomCertService.name) constructor(private readonly clusterService: ClusterService) {} - // Read a certificate for a given website using cert-manager.io CRD - async readWebsiteDomainCert(region: Region, website: WebsiteHosting) { + async readDomainCert(region: Region, appid: string, name: string) { try { // Get the namespace based on the application ID - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create a Kubernetes API client for the specified region const api = this.clusterService.makeCustomObjectApi(region) @@ -26,7 +26,7 @@ export class ApisixCustomCertService { 'v1', namespace, 'certificates', - website._id.toString(), + name, ) return res.body @@ -38,10 +38,15 @@ export class ApisixCustomCertService { } } - // Create a certificate for a given website using cert-manager.io CRD - async createWebsiteDomainCert(region: Region, website: WebsiteHosting) { + async createDomainCert( + region: Region, + appid: string, + name: string, + domain: string, + labels: Record, + ) { // Get the namespace based on the application ID - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create a Kubernetes API client for the specified region const api = this.clusterService.makeObjectApi(region) @@ -51,18 +56,14 @@ export class ApisixCustomCertService { kind: 'Certificate', // Set the metadata for the Certificate resource metadata: { - name: website._id.toString(), + name, namespace, - labels: { - 'laf.dev/website': website._id.toString(), - 'laf.dev/website-domain': website.domain, - [LABEL_KEY_APP_ID]: website.appid, - }, + labels, }, // Define the specification for the Certificate resource spec: { - secretName: website._id.toString(), - dnsNames: [website.domain], + secretName: name, + dnsNames: [domain], issuerRef: { name: ServerConfig.CertManagerIssuerName, kind: 'ClusterIssuer', @@ -72,10 +73,9 @@ export class ApisixCustomCertService { return res.body } - // Delete a certificate for a given website using cert-manager.io CRD - async deleteWebsiteDomainCert(region: Region, website: WebsiteHosting) { + async deleteDomainCert(region: Region, appid: string, name: string) { // Get the namespace based on the application ID - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create a Kubernetes API client for the specified region const api = this.clusterService.makeObjectApi(region) @@ -84,7 +84,7 @@ export class ApisixCustomCertService { apiVersion: 'cert-manager.io/v1', kind: 'Certificate', metadata: { - name: website._id.toString(), + name, namespace, }, }) @@ -95,7 +95,7 @@ export class ApisixCustomCertService { apiVersion: 'v1', kind: 'Secret', metadata: { - name: website._id.toString(), + name, namespace, }, }) @@ -108,11 +108,10 @@ export class ApisixCustomCertService { return res.body } - // Read an ApisixTls resource for a given website using apisix.apache.org CRD - async readWebsiteApisixTls(region: Region, website: WebsiteHosting) { + async readApisixTls(region: Region, appid: string, name: string) { try { // Get the namespace based on the application ID - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create an API object for the specified region const api = this.clusterService.makeCustomObjectApi(region) @@ -122,7 +121,7 @@ export class ApisixCustomCertService { 'v2', namespace, 'apisixtlses', - website._id.toString(), + name, ) return res.body } catch (err) { @@ -133,10 +132,15 @@ export class ApisixCustomCertService { } } - // Create an ApisixTls resource for a given website using apisix.apache.org CRD - async createWebsiteApisixTls(region: Region, website: WebsiteHosting) { + async createApisixTls( + region: Region, + appid: string, + name: string, + domain: string, + labels: Record, + ) { // Get the namespace based on the application ID - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create an API object for the specified region const api = this.clusterService.makeObjectApi(region) @@ -146,19 +150,15 @@ export class ApisixCustomCertService { kind: 'ApisixTls', // Set the metadata for the ApisixTls resource metadata: { - name: website._id.toString(), + name, namespace, - labels: { - 'laf.dev/website': website._id.toString(), - 'laf.dev/website-domain': website.domain, - [LABEL_KEY_APP_ID]: website.appid, - }, + labels, }, // Define the specification for the ApisixTls resource spec: { - hosts: [website.domain], + hosts: [domain], secret: { - name: website._id.toString(), + name, namespace, }, }, @@ -166,10 +166,9 @@ export class ApisixCustomCertService { return res.body } - // Deletes the APISIX TLS configuration for a specific website domain - async deleteWebsiteApisixTls(region: Region, website: WebsiteHosting) { + async deleteApisixTls(region: Region, appid: string, name: string) { // Get the application namespace using the website's appid - const namespace = GetApplicationNamespaceByAppId(website.appid) + const namespace = GetApplicationNamespaceByAppId(appid) // Create an API object for the specified region const api = this.clusterService.makeObjectApi(region) @@ -179,11 +178,157 @@ export class ApisixCustomCertService { apiVersion: 'apisix.apache.org/v2', kind: 'ApisixTls', metadata: { - name: website._id.toString(), + name, namespace, }, }) return res.body } + + // Read a certificate for a given website using cert-manager.io CRD + async readWebsiteDomainCert(region: Region, website: WebsiteHosting) { + return await this.readDomainCert( + region, + website.appid, + website._id.toString(), + ) + } + + // Create a certificate for a given website using cert-manager.io CRD + async createWebsiteDomainCert(region: Region, website: WebsiteHosting) { + return await this.createDomainCert( + region, + website.appid, + website._id.toString(), + website.domain, + { + 'laf.dev/website': website._id.toString(), + 'laf.dev/website-domain': website.domain, + [LABEL_KEY_APP_ID]: website.appid, + }, + ) + } + + // Delete a certificate for a given website using cert-manager.io CRD + async deleteWebsiteDomainCert(region: Region, website: WebsiteHosting) { + return await this.deleteDomainCert( + region, + website.appid, + website._id.toString(), + ) + } + + // Read an ApisixTls resource for a given website using apisix.apache.org CRD + async readWebsiteApisixTls(region: Region, website: WebsiteHosting) { + return await this.readApisixTls( + region, + website.appid, + website._id.toString(), + ) + } + + // Create an ApisixTls resource for a given website using apisix.apache.org CRD + async createWebsiteApisixTls(region: Region, website: WebsiteHosting) { + return await this.createApisixTls( + region, + website.appid, + website._id.toString(), + website.domain, + { + 'laf.dev/website': website._id.toString(), + 'laf.dev/website-domain': website.domain, + [LABEL_KEY_APP_ID]: website.appid, + }, + ) + } + + // Deletes the APISIX TLS configuration for a specific website domain + async deleteWebsiteApisixTls(region: Region, website: WebsiteHosting) { + return await this.deleteApisixTls( + region, + website.appid, + website._id.toString(), + ) + } + + // ========= App Custom Domain + // Read a certificate for app custom domain using cert-manager.io CRD + async readAppCustomDomainCert(region: Region, runtimeDomain: RuntimeDomain) { + return await this.readDomainCert( + region, + runtimeDomain.appid, + runtimeDomain.appid, + ) + } + + // Create a certificate for app custom domain using cert-manager.io CRD + async createAppCustomDomainCert( + region: Region, + runtimeDomain: RuntimeDomain, + ) { + return await this.createDomainCert( + region, + runtimeDomain.appid, + runtimeDomain.appid, + runtimeDomain.customDomain, + { + 'laf.dev/app-custom-domain': runtimeDomain.customDomain, + [LABEL_KEY_APP_ID]: runtimeDomain.appid, + }, + ) + } + + // Delete a certificate for app custom domain using cert-manager.io CRD + async deleteAppCustomDomainCert( + region: Region, + runtimeDomain: RuntimeDomain, + ) { + return await this.deleteDomainCert( + region, + runtimeDomain.appid, + runtimeDomain.appid, + ) + } + + // Read an ApisixTls resource for app custom domain using apisix.apache.org CRD + async readAppCustomDomainApisixTls( + region: Region, + runtimeDomain: RuntimeDomain, + ) { + return await this.readApisixTls( + region, + runtimeDomain.appid, + runtimeDomain.appid, + ) + } + + // Create an ApisixTls resource for app custom domain using apisix.apache.org CRD + async createAppCustomDomainApisixTls( + region: Region, + runtimeDomain: RuntimeDomain, + ) { + return await this.createApisixTls( + region, + runtimeDomain.appid, + runtimeDomain.appid, + runtimeDomain.customDomain, + { + 'laf.dev/app-custom-domain': runtimeDomain.customDomain, + [LABEL_KEY_APP_ID]: runtimeDomain.appid, + }, + ) + } + + // Deletes the APISIX TLS configuration for app custom domain + async deleteAppCustomDomainApisixTls( + region: Region, + runtimeDomain: RuntimeDomain, + ) { + return await this.deleteApisixTls( + region, + runtimeDomain.appid, + runtimeDomain.appid, + ) + } } diff --git a/server/src/gateway/apisix.service.ts b/server/src/gateway/apisix.service.ts index 49be32881e..329da0fadb 100644 --- a/server/src/gateway/apisix.service.ts +++ b/server/src/gateway/apisix.service.ts @@ -3,12 +3,37 @@ import { Injectable, Logger } from '@nestjs/common' import { GetApplicationNamespaceByAppId } from '../utils/getter' import { Region } from 'src/region/entities/region' import { WebsiteHosting } from 'src/website/entities/website' +import { RuntimeDomain } from './entities/runtime-domain' @Injectable() export class ApisixService { private readonly logger = new Logger(ApisixService.name) - constructor(private readonly httpService: HttpService) { } + constructor(private readonly httpService: HttpService) {} + + get gzipConf() { + return { + gzip: { + comp_level: 6, + min_length: 100, + types: [ + 'text/plain', + 'text/css', + 'text/html', + 'text/xml', + 'text/javascript', + 'application/json', + 'application/x-javascript', + 'application/javascript', + 'image/bmp', + 'image/png', + 'font/ttf', + 'font/otf', + 'font/eot', + ], + }, + } + } async createAppRoute(region: Region, appid: string, domain: string) { const host = domain @@ -39,6 +64,7 @@ export class ApisixService { }, plugins: { cors: {}, + ...this.gzipConf, }, enable_websocket: true, } @@ -54,6 +80,53 @@ export class ApisixService { return res } + async createAppCustomRoute(region: Region, runtimeDomain: RuntimeDomain) { + const appid = runtimeDomain.appid + const host = runtimeDomain.customDomain + const namespace = GetApplicationNamespaceByAppId(appid) + const upstreamNode = `${appid}.${namespace}:8000` + const upstreamHost = runtimeDomain.domain + + const id = `app-custom-${appid}` + const data = { + name: id, + labels: { + type: 'runtime', + appid: appid, + }, + uri: '/*', + hosts: [host], + priority: 9, + upstream: { + type: 'roundrobin', + pass_host: 'rewrite', + upstream_host: upstreamHost, + nodes: { + [upstreamNode]: 1, + }, + }, + timeout: { + connect: 60, + send: 600, + read: 600, + }, + plugins: { + cors: {}, + ...this.gzipConf, + }, + enable_websocket: true, + } + + const res = await this.putRoute(region, id, data) + return res + } + + async deleteAppCustomRoute(region: Region, appid: string) { + const id = `app-custom-${appid}` + const res = await this.deleteRoute(region, id) + return res + } + async createBucketRoute(region: Region, bucketName: string, domain: string) { const host = domain @@ -84,6 +157,7 @@ export class ApisixService { }, plugins: { cors: {}, + ...this.gzipConf, }, } @@ -133,14 +207,15 @@ export class ApisixService { read: 60, }, plugins: { - "ext-plugin-post-req": { - "conf": [ + 'ext-plugin-post-req': { + conf: [ { - "name": "try-path", - "value": `{\"paths\":[\"$uri\", \"$uri/\", \"/index.html\"], \"host\": \"http://${upstreamNode}/${website.bucketName}\"}` - } - ] - } + name: 'try-path', + value: `{\"paths\":[\"$uri\", \"$uri/\", \"/index.html\"], \"host\": \"http://${upstreamNode}/${website.bucketName}\"}`, + }, + ], + }, + ...this.gzipConf, }, } diff --git a/server/src/gateway/entities/runtime-domain.ts b/server/src/gateway/entities/runtime-domain.ts index 3a876f763a..b3344d24e5 100644 --- a/server/src/gateway/entities/runtime-domain.ts +++ b/server/src/gateway/entities/runtime-domain.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ObjectId } from 'mongodb' export enum DomainPhase { @@ -24,6 +24,9 @@ export class RuntimeDomain { @ApiProperty() domain: string + @ApiPropertyOptional() + customDomain?: string + @ApiProperty({ enum: DomainState }) state: DomainState diff --git a/server/src/gateway/runtime-domain-task.service.ts b/server/src/gateway/runtime-domain-task.service.ts index 8b169310ad..b5b3e85876 100644 --- a/server/src/gateway/runtime-domain-task.service.ts +++ b/server/src/gateway/runtime-domain-task.service.ts @@ -10,6 +10,8 @@ import { DomainState, RuntimeDomain, } from './entities/runtime-domain' +import { ApisixCustomCertService } from './apisix-custom-cert.service' +import { isConditionTrue } from 'src/utils/getter' @Injectable() export class RuntimeDomainTaskService { @@ -19,6 +21,7 @@ export class RuntimeDomainTaskService { constructor( private readonly apisixService: ApisixService, private readonly regionService: RegionService, + private readonly certService: ApisixCustomCertService, ) {} @Cron(CronExpression.EVERY_SECOND) @@ -90,6 +93,62 @@ export class RuntimeDomainTaskService { this.logger.debug(route) } + // custom domain + if (doc.customDomain) { + const id = `app-custom-${doc.appid}` + const route = await this.apisixService.getRoute(region, id) + if (!route) { + await this.apisixService.createAppCustomRoute(region, doc) + this.logger.log('app custom route created: ' + doc.appid) + this.logger.debug(route) + } + + { + // create custom certificate if custom domain is set + const waitingTime = Date.now() - doc.updatedAt.getTime() + + // create custom domain certificate + let cert = await this.certService.readAppCustomDomainCert(region, doc) + if (!cert) { + cert = await this.certService.createAppCustomDomainCert(region, doc) + this.logger.log(`create app custom domain cert: ${doc.appid}`) + // return to wait for cert to be ready + return await this.relock(doc.appid, waitingTime) + } + + // check if cert status is Ready + const conditions = (cert as any).status?.conditions || [] + if (!isConditionTrue('Ready', conditions)) { + this.logger.log(`app custom domain cert is not ready: ${doc.appid}`) + // return to wait for cert to be ready + return await this.relock(doc.appid, waitingTime) + } + + // config custom domain certificate to apisix + let apisixTls = await this.certService.readAppCustomDomainApisixTls( + region, + doc, + ) + if (!apisixTls) { + apisixTls = await this.certService.createAppCustomDomainApisixTls( + region, + doc, + ) + this.logger.log(`create app custom domain apisix tls: ${doc.appid}`) + // return to wait for tls config to be ready + return await this.relock(doc.appid, waitingTime) + } + + // check if apisix tls status is Ready + const apisixTlsConditions = (apisixTls as any).status?.conditions || [] + if (!isConditionTrue('ResourcesAvailable', apisixTlsConditions)) { + this.logger.log(`website apisix tls is not ready: ${doc.appid}`) + // return to wait for tls config to be ready + return await this.relock(doc.appid, waitingTime) + } + } + } + // update phase to `Created` await db.collection('RuntimeDomain').updateOne( { _id: doc._id, phase: DomainPhase.Creating }, @@ -127,19 +186,59 @@ export class RuntimeDomainTaskService { assert(region, 'region not found') // delete route first if exists - const id = `app-${doc.appid}` - const route = await this.apisixService.getRoute(region, id) - if (route) { - await this.apisixService.deleteAppRoute(region, doc.appid) - this.logger.log('app route deleted: ' + doc.appid) - this.logger.debug(route) + { + const id = `app-${doc.appid}` + const route = await this.apisixService.getRoute(region, id) + if (route) { + await this.apisixService.deleteAppRoute(region, doc.appid) + this.logger.log('app route deleted: ' + doc.appid) + this.logger.debug(route) + } + } + + { + const id = `app-custom-${doc.appid}` + const route = await this.apisixService.getRoute(region, id) + if (route) { + await this.apisixService.deleteAppCustomRoute(region, doc.appid) + this.logger.log('app custom route deleted: ' + doc.appid) + this.logger.debug(route) + } + + // delete app custom certificate if custom domain is set + const waitingTime = Date.now() - doc.updatedAt.getTime() + + // delete custom domain certificate + const cert = await this.certService.readAppCustomDomainCert(region, doc) + if (cert) { + await this.certService.deleteAppCustomDomainCert(region, doc) + this.logger.log(`delete app custom domain cert: ${doc.appid}`) + // return to wait for cert to be deleted + return await this.relock(doc.appid, waitingTime) + } + + // delete custom domain tls config from apisix + const tls = await this.certService.readAppCustomDomainApisixTls( + region, + doc, + ) + if (tls) { + await this.certService.deleteAppCustomDomainApisixTls(region, doc) + this.logger.log(`delete app custom domain tls: ${doc.appid}`) + // return to wait for tls config to be deleted + return this.relock(doc.appid, waitingTime) + } } // update phase to `Deleted` await db.collection('RuntimeDomain').updateOne( { _id: doc._id, phase: DomainPhase.Deleting }, { - $set: { phase: DomainPhase.Deleted, lockedAt: TASK_LOCK_INIT_TIME }, + $set: { + phase: DomainPhase.Deleted, + lockedAt: TASK_LOCK_INIT_TIME, + updatedAt: new Date(), + }, }, ) @@ -208,4 +307,15 @@ export class RuntimeDomainTaskService { phase: DomainPhase.Deleted, }) } + + /** + * Relock application by appid, lockedTime is in milliseconds + */ + async relock(appid: string, lockedTime = 0) { + const db = SystemDatabase.db + const lockedAt = new Date(Date.now() - 1000 * this.lockTimeout + lockedTime) + await db + .collection('RuntimeDomain') + .updateOne({ appid }, { $set: { lockedAt } }) + } } diff --git a/server/src/gateway/runtime-domain.service.ts b/server/src/gateway/runtime-domain.service.ts index 4921025b87..5d635602ce 100644 --- a/server/src/gateway/runtime-domain.service.ts +++ b/server/src/gateway/runtime-domain.service.ts @@ -8,6 +8,7 @@ import { DomainState, RuntimeDomain, } from './entities/runtime-domain' +import * as dns from 'node:dns' @Injectable() export class RuntimeDomainService { @@ -38,6 +39,68 @@ export class RuntimeDomainService { return await this.findOne(appid) } + async checkResolved(appid: string, customDomain: string) { + const runtimeDomain = await this.db + .collection('RuntimeDomain') + .findOne({ + appid, + }) + + const cnameTarget = runtimeDomain.domain + + // check domain is available + const resolver = new dns.promises.Resolver({ timeout: 3000, tries: 1 }) + const result = await resolver + .resolveCname(customDomain as string) + .catch(() => { + return + }) + + if (!result) return false + if (false === (result || []).includes(cnameTarget)) return false + return true + } + + async bindCustomDomain(appid: string, customDomain: string) { + const res = await this.db + .collection('RuntimeDomain') + .findOneAndUpdate( + { appid }, + { + $set: { + customDomain, + phase: DomainPhase.Deleting, + updatedAt: new Date(), + }, + }, + { + returnDocument: 'after', + }, + ) + + return res.value + } + + async removeCustomDomain(appid: string) { + const res = await this.db + .collection('RuntimeDomain') + .findOneAndUpdate( + { appid }, + { + $set: { + customDomain: null, + phase: DomainPhase.Deleting, + updatedAt: new Date(), + }, + }, + { + returnDocument: 'after', + }, + ) + + return res.value + } + /** * Find an app domain in database */