Skip to content

Commit e1e597d

Browse files
authored
feat(server): implement website hosting task (#774)
1 parent bd983a0 commit e1e597d

File tree

5 files changed

+198
-37
lines changed

5 files changed

+198
-37
lines changed

server/prisma/schema.prisma

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ model Database {
275275
user String
276276
password String
277277
state DatabaseState @default(Active)
278-
phase DatabasePhase @default(Created)
278+
phase DatabasePhase @default(Creating)
279279
lockedAt DateTime
280280
createdAt DateTime @default(now())
281281
updatedAt DateTime @updatedAt

server/src/application/application-task.service.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { Injectable, Logger } from '@nestjs/common'
22
import { Cron, CronExpression } from '@nestjs/schedule'
3-
import { Application, ApplicationPhase, ApplicationState } from '@prisma/client'
3+
import {
4+
Application,
5+
ApplicationPhase,
6+
ApplicationState,
7+
DatabasePhase,
8+
DomainPhase,
9+
StoragePhase,
10+
} from '@prisma/client'
411
import * as assert from 'node:assert'
512
import { StorageService } from '../storage/storage.service'
613
import { DatabaseService } from '../database/database.service'
@@ -89,10 +96,7 @@ export class ApplicationTaskService {
8996
let storage = await this.storageService.findOne(appid)
9097
if (!storage) {
9198
this.logger.log(`Creating storage for application ${appid}`)
92-
const res = await this.storageService.create(app.appid)
93-
if (res) {
94-
storage = res
95-
}
99+
storage = await this.storageService.create(app.appid)
96100
}
97101

98102
// reconcile database
@@ -109,9 +113,18 @@ export class ApplicationTaskService {
109113
gateway = await this.gatewayService.create(appid)
110114
}
111115

112-
if (!gateway) return await this.unlock(appid)
113-
if (!storage) return await this.unlock(appid)
114-
if (!database) return await this.unlock(appid)
116+
// waiting resources' phase to be `Created`
117+
if (gateway?.phase !== DomainPhase.Created) {
118+
return await this.unlock(appid)
119+
}
120+
121+
if (storage?.phase !== StoragePhase.Created) {
122+
return await this.unlock(appid)
123+
}
124+
125+
if (database?.phase !== DatabasePhase.Created) {
126+
return await this.unlock(appid)
127+
}
115128

116129
// update application phase to `Created`
117130
const updated = await db.collection<Application>('Application').updateOne(
@@ -257,7 +270,7 @@ export class ApplicationTaskService {
257270
},
258271
},
259272
)
260-
if (updated.modifiedCount > 0) this.logger.debug('unlocked app: ' + appid)
273+
if (updated.modifiedCount > 0) this.logger.debug(`unlocked app: ${appid}`)
261274
}
262275

263276
/**

server/src/gateway/apisix.service.ts

+72-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HttpService } from '@nestjs/axios'
22
import { Injectable, Logger } from '@nestjs/common'
3-
import { Region } from '@prisma/client'
3+
import { Region, WebsiteHosting } from '@prisma/client'
44
import { GetApplicationNamespaceById } from '../utils/getter'
55

66
@Injectable()
@@ -17,6 +17,10 @@ export class ApisixService {
1717
const id = `app-${appid}`
1818
const data = {
1919
name: id,
20+
labels: {
21+
type: 'runtime',
22+
appid: appid,
23+
},
2024
uri: '/*',
2125
hosts: [host],
2226
priority: 9,
@@ -56,6 +60,10 @@ export class ApisixService {
5660
const id = `bucket-${bucketName}`
5761
const data = {
5862
name: id,
63+
labels: {
64+
type: 'bucket',
65+
bucket: bucketName,
66+
},
5967
uri: '/*',
6068
hosts: [host],
6169
priority: 9,
@@ -85,18 +93,73 @@ export class ApisixService {
8593
return res
8694
}
8795

96+
async createWebsiteRoute(
97+
region: Region,
98+
website: WebsiteHosting,
99+
bucketDomain: string,
100+
) {
101+
const host = website.domain
102+
const minioUrl = new URL(region.storageConf.internalEndpoint)
103+
const upstreamNode = minioUrl.host
104+
const upstreamHost = bucketDomain
105+
106+
const id = `${website['_id']}`
107+
const name = `website-${id}`
108+
const data = {
109+
name: name,
110+
labels: {
111+
customDomain: website.isCustom ? 'true' : 'false',
112+
type: 'website',
113+
},
114+
uri: '/*',
115+
hosts: [host],
116+
priority: 20,
117+
upstream: {
118+
type: 'roundrobin',
119+
pass_host: 'rewrite',
120+
upstream_host: upstreamHost,
121+
nodes: {
122+
[upstreamNode]: 1,
123+
},
124+
},
125+
timeout: {
126+
connect: 60,
127+
send: 60,
128+
read: 60,
129+
},
130+
plugins: {
131+
'proxy-rewrite': {
132+
regex_uri: ['/$', '/index.html'],
133+
},
134+
},
135+
}
136+
137+
const res = await this.putRoute(region, id, data)
138+
return res
139+
}
140+
141+
async deleteWebsiteRoute(region: Region, website: WebsiteHosting) {
142+
const id = `${website['_id']}`
143+
const res = await this.deleteRoute(region, id)
144+
return res
145+
}
146+
88147
async putRoute(region: Region, id: string, data: any) {
89148
const conf = region.gatewayConf
90149
const api_url = `${conf.apiUrl}/routes/${id}`
91150

92-
const res = await this.httpService.axiosRef.put(api_url, data, {
93-
headers: {
94-
'X-API-KEY': conf.apiKey,
95-
'Content-Type': 'application/json',
96-
},
97-
})
98-
99-
return res.data
151+
try {
152+
const res = await this.httpService.axiosRef.put(api_url, data, {
153+
headers: {
154+
'X-API-KEY': conf.apiKey,
155+
'Content-Type': 'application/json',
156+
},
157+
})
158+
return res.data
159+
} catch (error) {
160+
this.logger.error(error, error.response.data)
161+
return null
162+
}
100163
}
101164

102165
async deleteRoute(region: Region, id: string) {

server/src/gateway/website-task.service.ts

+90-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import { Injectable, Logger } from '@nestjs/common'
22
import { Cron, CronExpression } from '@nestjs/schedule'
3-
import { DomainPhase, DomainState, WebsiteHosting } from '@prisma/client'
3+
import {
4+
BucketDomain,
5+
DomainPhase,
6+
DomainState,
7+
WebsiteHosting,
8+
} from '@prisma/client'
49
import { times } from 'lodash'
510
import { TASK_LOCK_INIT_TIME } from 'src/constants'
611
import { SystemDatabase } from 'src/database/system-database'
12+
import { RegionService } from 'src/region/region.service'
13+
import * as assert from 'node:assert'
14+
import { ApisixService } from './apisix.service'
715

816
@Injectable()
917
export class WebsiteTaskService {
1018
readonly lockTimeout = 30 // in second
1119
readonly concurrency = 1 // concurrency count
1220
private readonly logger = new Logger(WebsiteTaskService.name)
1321

22+
constructor(
23+
private readonly regionService: RegionService,
24+
private readonly apisixService: ApisixService,
25+
) {}
26+
1427
@Cron(CronExpression.EVERY_SECOND)
1528
async tick() {
16-
return
1729
// Phase `Creating` -> `Created`
1830
times(this.concurrency, () => this.handleCreatingPhase())
1931

@@ -41,7 +53,7 @@ export class WebsiteTaskService {
4153
async handleCreatingPhase() {
4254
const db = SystemDatabase.db
4355

44-
const doc = await db
56+
const res = await db
4557
.collection<WebsiteHosting>('WebsiteHosting')
4658
.findOneAndUpdate(
4759
{
@@ -57,10 +69,50 @@ export class WebsiteTaskService {
5769
},
5870
)
5971

60-
if (doc.value) {
61-
// TODO
62-
// create website route
63-
// update phase to `Created`
72+
if (!res.value) return
73+
74+
this.logger.debug(res.value)
75+
// get region by appid
76+
const site = res.value
77+
const region = await this.regionService.findByAppId(site.appid)
78+
assert(region, 'region not found')
79+
80+
// get bucket domain
81+
const bucketDomain = await db
82+
.collection<BucketDomain>('BucketDomain')
83+
.findOne({
84+
appid: site.appid,
85+
bucketName: site.bucketName,
86+
})
87+
88+
assert(bucketDomain, 'bucket domain not found')
89+
90+
// create website route
91+
const route = await this.apisixService.createWebsiteRoute(
92+
region,
93+
site,
94+
bucketDomain.domain,
95+
)
96+
this.logger.debug(`create website route: `, route)
97+
98+
// update phase to `Created`
99+
const updated = await db
100+
.collection<WebsiteHosting>('WebsiteHosting')
101+
.updateOne(
102+
{
103+
_id: site._id,
104+
phase: DomainPhase.Creating,
105+
},
106+
{
107+
$set: {
108+
phase: DomainPhase.Created,
109+
lockedAt: TASK_LOCK_INIT_TIME,
110+
},
111+
},
112+
)
113+
114+
if (updated.modifiedCount !== 1) {
115+
this.logger.error(`update website hosting phase failed: ${site._id}`)
64116
}
65117
}
66118

@@ -72,7 +124,7 @@ export class WebsiteTaskService {
72124
async handleDeletingPhase() {
73125
const db = SystemDatabase.db
74126

75-
const doc = await db
127+
const res = await db
76128
.collection<WebsiteHosting>('WebsiteHosting')
77129
.findOneAndUpdate(
78130
{
@@ -88,10 +140,36 @@ export class WebsiteTaskService {
88140
},
89141
)
90142

91-
if (doc.value) {
92-
// TODO
93-
// delete website route
94-
// update phase to `Deleted`
143+
if (!res.value) return
144+
145+
// get region by appid
146+
const site = res.value
147+
const region = await this.regionService.findByAppId(site.appid)
148+
assert(region, 'region not found')
149+
150+
// delete website route
151+
const route = await this.apisixService.deleteWebsiteRoute(region, site)
152+
153+
this.logger.debug(`delete website route: `, route)
154+
155+
// update phase to `Deleted`
156+
const updated = await db
157+
.collection<WebsiteHosting>('WebsiteHosting')
158+
.updateOne(
159+
{
160+
_id: site._id,
161+
phase: DomainPhase.Deleting,
162+
},
163+
{
164+
$set: {
165+
phase: DomainPhase.Deleted,
166+
lockedAt: TASK_LOCK_INIT_TIME,
167+
},
168+
},
169+
)
170+
171+
if (updated.modifiedCount > 1) {
172+
this.logger.error(`update website hosting phase failed: ${site._id}`)
95173
}
96174
}
97175

server/src/storage/storage.service.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,25 @@ export class StorageService {
2222
const accessKey = appid
2323
const secretKey = GenerateAlphaNumericPassword(64)
2424

25-
// create storage user in minio
26-
const r0 = await this.minioService.createUser(region, accessKey, secretKey)
27-
if (r0.error) {
28-
this.logger.error(r0.error)
29-
return false
25+
// create storage user in minio if not exists
26+
const minioUser = await this.minioService.getUser(region, accessKey)
27+
if (!minioUser) {
28+
const r0 = await this.minioService.createUser(
29+
region,
30+
accessKey,
31+
secretKey,
32+
)
33+
if (r0.error) {
34+
this.logger.error(r0.error)
35+
return null
36+
}
3037
}
3138

3239
// add storage user to common user group in minio
3340
const r1 = await this.minioService.addUserToGroup(region, accessKey)
3441
if (r1.error) {
3542
this.logger.error(r1.error)
36-
return false
43+
return null
3744
}
3845

3946
// create storage user in database

0 commit comments

Comments
 (0)