@atomservice/config 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +37 -0
- package/server/.env +3 -0
- package/server/Containerfile +50 -0
- package/server/package.json +18 -0
- package/server/src/db/client.ts +49 -0
- package/server/src/db/consts.ts +3 -0
- package/server/src/db/migrations.ts +106 -0
- package/server/src/db/types.ts +101 -0
- package/server/src/http/response.ts +82 -0
- package/server/src/http/router.ts +27 -0
- package/server/src/main.ts +37 -0
- package/server/src/modules/app.ts +100 -0
- package/server/src/modules/env.ts +86 -0
- package/server/src/modules/inspect.ts +67 -0
- package/server/src/modules/member.ts +59 -0
- package/server/src/modules/project.ts +98 -0
- package/server/src/modules/release.ts +328 -0
- package/server/src/modules/token.ts +58 -0
- package/server/src/modules/user.ts +74 -0
- package/server/src/shared/auth.ts +58 -0
- package/server/src/shared/bootstrap.ts +29 -0
- package/server/src/shared/consts.ts +10 -0
- package/server/src/shared/env.ts +55 -0
- package/server/src/shared/jsonschema.ts +74 -0
- package/server/src/shared/password.ts +7 -0
- package/server/src/shared/resolve.ts +33 -0
- package/server/src/shared/semver.ts +33 -0
- package/server/src/shared/serialize.ts +84 -0
- package/server/src/shared/token.ts +59 -0
- package/server/src/shared/types.ts +25 -0
- package/server/src/shared/utils.ts +17 -0
- package/server/tsconfig.json +27 -0
- package/src/config.compose.ts +59 -0
- package/src/config.consts.ts +5 -0
- package/src/config.instance.ts +26 -0
- package/src/config.options.ts +58 -0
- package/src/config.service.ts +121 -0
- package/src/config.types.ts +3 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { tmpl } from "@atomservice/core"
|
|
2
|
+
import { CONFIG_DEFAULT_PORT } from "./config.consts.ts"
|
|
3
|
+
|
|
4
|
+
export interface ConfigComposeOptions {
|
|
5
|
+
image: string
|
|
6
|
+
dataDir: string
|
|
7
|
+
adminName?: string
|
|
8
|
+
adminEmail?: string
|
|
9
|
+
adminPassword?: string
|
|
10
|
+
environment?: Record<string, string>
|
|
11
|
+
hostPort?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function configCompose(id: string, opts: ConfigComposeOptions): string {
|
|
15
|
+
const port = CONFIG_DEFAULT_PORT
|
|
16
|
+
const portsBlock = opts.hostPort ? `\n ports:\n - "${opts.hostPort}:${port}"` : ""
|
|
17
|
+
|
|
18
|
+
const lines: string[] = []
|
|
19
|
+
const push = (k: string, v: string | number | undefined) => {
|
|
20
|
+
if (v === undefined || v === "") return
|
|
21
|
+
lines.push(` - ${k}=${v}`)
|
|
22
|
+
}
|
|
23
|
+
push("CONFIG_ADMIN_NAME", opts.adminName)
|
|
24
|
+
push("CONFIG_ADMIN_EMAIL", opts.adminEmail)
|
|
25
|
+
push("CONFIG_ADMIN_PASSWORD", opts.adminPassword)
|
|
26
|
+
if (opts.environment) {
|
|
27
|
+
for (const [k, v] of Object.entries(opts.environment)) push(k, v)
|
|
28
|
+
}
|
|
29
|
+
const extraEnv = lines.length ? `\n${lines.join("\n")}` : ""
|
|
30
|
+
|
|
31
|
+
return tmpl`\
|
|
32
|
+
services:
|
|
33
|
+
${id}:
|
|
34
|
+
image: ${opts.image}
|
|
35
|
+
container_name: ${id}
|
|
36
|
+
environment:
|
|
37
|
+
- CONFIG_PORT=${port}
|
|
38
|
+
- CONFIG_DB_PATH=/data/config.db${extraEnv}
|
|
39
|
+
volumes:
|
|
40
|
+
- ${opts.dataDir}:/data
|
|
41
|
+
expose:
|
|
42
|
+
- "${port}"${portsBlock}
|
|
43
|
+
networks:
|
|
44
|
+
- atomservice
|
|
45
|
+
restart: unless-stopped
|
|
46
|
+
healthcheck:
|
|
47
|
+
test: ["CMD", "curl", "-sf", "http://localhost:${port}/v1/health"]
|
|
48
|
+
interval: 10s
|
|
49
|
+
timeout: 5s
|
|
50
|
+
retries: 5
|
|
51
|
+
start_period: 15s
|
|
52
|
+
start_period: 20s
|
|
53
|
+
|
|
54
|
+
networks:
|
|
55
|
+
atomservice:
|
|
56
|
+
name: atomservice
|
|
57
|
+
external: true
|
|
58
|
+
`
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ConfigInstance {
|
|
2
|
+
/**
|
|
3
|
+
* 容器主机名
|
|
4
|
+
*
|
|
5
|
+
* - 同容器名
|
|
6
|
+
* - 容器网络内可直接访问
|
|
7
|
+
*/
|
|
8
|
+
readonly host: string
|
|
9
|
+
/**
|
|
10
|
+
* 容器内 HTTP 监听端口
|
|
11
|
+
*/
|
|
12
|
+
readonly port: number
|
|
13
|
+
/**
|
|
14
|
+
* 容器网络内访问地址
|
|
15
|
+
*
|
|
16
|
+
* - 如 `http://config:4921/`
|
|
17
|
+
*/
|
|
18
|
+
readonly url: string
|
|
19
|
+
/**
|
|
20
|
+
* 对外域名地址
|
|
21
|
+
*
|
|
22
|
+
* - 如 `https://config.example.com/`
|
|
23
|
+
* - 仅配置 `domain` 后可用
|
|
24
|
+
*/
|
|
25
|
+
readonly externalUrl: string | undefined
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CallableService } from "@atomservice/core"
|
|
2
|
+
import type { GatewayInstance } from "@atomservice/gateway"
|
|
3
|
+
|
|
4
|
+
export interface ConfigOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 实例标识
|
|
7
|
+
*
|
|
8
|
+
* @default "default"
|
|
9
|
+
*/
|
|
10
|
+
id?: string
|
|
11
|
+
/**
|
|
12
|
+
* 容器对外端口
|
|
13
|
+
*
|
|
14
|
+
* - 不填则不对外暴露,仅容器网络内访问
|
|
15
|
+
*/
|
|
16
|
+
hostPort?: number
|
|
17
|
+
/**
|
|
18
|
+
* 对外域名
|
|
19
|
+
*
|
|
20
|
+
* - 填写后自动在网关注册反代路由
|
|
21
|
+
* - 需要和 `gateway` 原子一起使用
|
|
22
|
+
*/
|
|
23
|
+
domain?: string
|
|
24
|
+
/**
|
|
25
|
+
* 初始超级管理员用户名
|
|
26
|
+
*
|
|
27
|
+
* - 与 `adminEmail` 和 `adminPassword` 一起填写则容器首次启动时自动创建超管账号
|
|
28
|
+
*
|
|
29
|
+
* @default "admin"
|
|
30
|
+
*/
|
|
31
|
+
adminName?: string
|
|
32
|
+
/**
|
|
33
|
+
* 初始超级管理员邮箱
|
|
34
|
+
*
|
|
35
|
+
* - 与 `adminPassword` 一起填写则容器首次启动时自动创建超管账号
|
|
36
|
+
* - 不填则需首次访问后通过 API 手动创建
|
|
37
|
+
*/
|
|
38
|
+
adminEmail?: string
|
|
39
|
+
/**
|
|
40
|
+
* 初始超级管理员密码
|
|
41
|
+
*
|
|
42
|
+
* - 见 `adminEmail`
|
|
43
|
+
*/
|
|
44
|
+
adminPassword?: string
|
|
45
|
+
/**
|
|
46
|
+
* 自定义环境变量
|
|
47
|
+
*
|
|
48
|
+
* - 透传到容器
|
|
49
|
+
*/
|
|
50
|
+
environment?: Record<string, string>
|
|
51
|
+
/**
|
|
52
|
+
* 关联的网关原子
|
|
53
|
+
*
|
|
54
|
+
* - 填写后自动向网关注册反代路由
|
|
55
|
+
* - 通常和 `domain` 一起使用
|
|
56
|
+
*/
|
|
57
|
+
gateway?: CallableService<GatewayInstance>
|
|
58
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import type { CallableService } from "@atomservice/core"
|
|
4
|
+
import { defineService, onDown, onHealth, onUp, useConfig, useLogger, useShell, useState } from "@atomservice/core"
|
|
5
|
+
import { configCompose } from "./config.compose.ts"
|
|
6
|
+
import { CONFIG_DEFAULT_PORT, CONFIG_IMAGE_REPO, CONFIG_VERSION } from "./config.consts.ts"
|
|
7
|
+
import type { ConfigInstance } from "./config.instance.ts"
|
|
8
|
+
import type { ConfigOptions } from "./config.options.ts"
|
|
9
|
+
import type { ConfigState } from "./config.types.ts"
|
|
10
|
+
|
|
11
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const SERVER_DIR = path.resolve(HERE, "..", "server")
|
|
13
|
+
|
|
14
|
+
export function config(opts: ConfigOptions = {}): CallableService<ConfigInstance> {
|
|
15
|
+
const serviceId = opts.id && opts.id !== "default" ? opts.id : "default"
|
|
16
|
+
const containerName = serviceId !== "default" ? `config-${serviceId}` : "config"
|
|
17
|
+
const version = CONFIG_VERSION
|
|
18
|
+
const imageTag = `${CONFIG_IMAGE_REPO}:${version}`
|
|
19
|
+
|
|
20
|
+
return defineService({
|
|
21
|
+
name: "config",
|
|
22
|
+
id: opts.id,
|
|
23
|
+
setup: () => {
|
|
24
|
+
const logger = useLogger()
|
|
25
|
+
const $ = useShell()
|
|
26
|
+
const conf = useConfig()
|
|
27
|
+
const gw = opts.gateway?.()
|
|
28
|
+
|
|
29
|
+
const dir = path.join(conf.root, containerName)
|
|
30
|
+
const composePath = path.join(dir, "compose.yml")
|
|
31
|
+
const dataDir = path.join(dir, "data")
|
|
32
|
+
|
|
33
|
+
const { save, watch, checkChanges } = useState<ConfigState>()
|
|
34
|
+
watch("version", {
|
|
35
|
+
risk: "low",
|
|
36
|
+
changed: (a, b) => `Config 镜像版本变更 ${a} → ${b},将重新构建`,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
async function ensureImage() {
|
|
40
|
+
const exists = await $`podman image exists ${imageTag}`.quiet().nothrow()
|
|
41
|
+
if (exists.exitCode === 0) return
|
|
42
|
+
logger.info(`未找到镜像 ${imageTag},开始构建(首次较慢)…`)
|
|
43
|
+
const build = await $`podman build -t ${imageTag} -f ${path.join(SERVER_DIR, "Containerfile")} ${SERVER_DIR}`
|
|
44
|
+
.quiet()
|
|
45
|
+
.nothrow()
|
|
46
|
+
if (build.exitCode !== 0) {
|
|
47
|
+
throw new Error(`镜像构建失败:${build.stderr.toString().slice(-500)}`)
|
|
48
|
+
}
|
|
49
|
+
logger.success(`镜像已构建 ${imageTag}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onUp(async () => {
|
|
53
|
+
const currentState: ConfigState = { version }
|
|
54
|
+
await checkChanges(currentState)
|
|
55
|
+
|
|
56
|
+
await $`mkdir -p ${dataDir}`
|
|
57
|
+
await ensureImage()
|
|
58
|
+
|
|
59
|
+
await Bun.write(
|
|
60
|
+
composePath,
|
|
61
|
+
configCompose(containerName, {
|
|
62
|
+
image: imageTag,
|
|
63
|
+
dataDir,
|
|
64
|
+
adminName: opts.adminName,
|
|
65
|
+
adminEmail: opts.adminEmail,
|
|
66
|
+
adminPassword: opts.adminPassword,
|
|
67
|
+
environment: opts.environment,
|
|
68
|
+
hostPort: opts.hostPort,
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
logger.info(`正在启动 ${containerName}`)
|
|
73
|
+
await $`podman compose -f ${composePath} up -d`
|
|
74
|
+
logger.success(`${containerName} 已启动`)
|
|
75
|
+
|
|
76
|
+
if (opts.domain && gw) {
|
|
77
|
+
logger.info(`注册路由 ${opts.domain} → ${containerName}:${CONFIG_DEFAULT_PORT}`)
|
|
78
|
+
await gw.registerRoute(opts.domain, `${containerName}:${CONFIG_DEFAULT_PORT}`)
|
|
79
|
+
logger.success(`路由已注册,访问 https://${opts.domain}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await save(currentState)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
onDown(async () => {
|
|
86
|
+
logger.info(`正在停止 ${containerName}`)
|
|
87
|
+
await $`podman compose -f ${composePath} down`
|
|
88
|
+
if (opts.domain && gw) await gw.unregisterRoute(opts.domain)
|
|
89
|
+
logger.success(`${containerName} 已停止`)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
onHealth(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const result =
|
|
95
|
+
await $`podman exec ${containerName} curl -sf http://localhost:${CONFIG_DEFAULT_PORT}/v1/health`
|
|
96
|
+
.quiet()
|
|
97
|
+
.nothrow()
|
|
98
|
+
if (result.exitCode === 0) return { status: "healthy" }
|
|
99
|
+
return { status: "unhealthy", message: `Config 健康检查返回 ${result.exitCode}` }
|
|
100
|
+
} catch {
|
|
101
|
+
return { status: "unhealthy", message: `容器 ${containerName} 不存在` }
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
get host() {
|
|
107
|
+
return containerName
|
|
108
|
+
},
|
|
109
|
+
get port() {
|
|
110
|
+
return CONFIG_DEFAULT_PORT
|
|
111
|
+
},
|
|
112
|
+
get url() {
|
|
113
|
+
return `http://${containerName}:${CONFIG_DEFAULT_PORT}/`
|
|
114
|
+
},
|
|
115
|
+
get externalUrl() {
|
|
116
|
+
return opts.domain ? `https://${opts.domain}/` : undefined
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
}
|
package/src/index.ts
ADDED