@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.
Files changed (39) hide show
  1. package/package.json +37 -0
  2. package/server/.env +3 -0
  3. package/server/Containerfile +50 -0
  4. package/server/package.json +18 -0
  5. package/server/src/db/client.ts +49 -0
  6. package/server/src/db/consts.ts +3 -0
  7. package/server/src/db/migrations.ts +106 -0
  8. package/server/src/db/types.ts +101 -0
  9. package/server/src/http/response.ts +82 -0
  10. package/server/src/http/router.ts +27 -0
  11. package/server/src/main.ts +37 -0
  12. package/server/src/modules/app.ts +100 -0
  13. package/server/src/modules/env.ts +86 -0
  14. package/server/src/modules/inspect.ts +67 -0
  15. package/server/src/modules/member.ts +59 -0
  16. package/server/src/modules/project.ts +98 -0
  17. package/server/src/modules/release.ts +328 -0
  18. package/server/src/modules/token.ts +58 -0
  19. package/server/src/modules/user.ts +74 -0
  20. package/server/src/shared/auth.ts +58 -0
  21. package/server/src/shared/bootstrap.ts +29 -0
  22. package/server/src/shared/consts.ts +10 -0
  23. package/server/src/shared/env.ts +55 -0
  24. package/server/src/shared/jsonschema.ts +74 -0
  25. package/server/src/shared/password.ts +7 -0
  26. package/server/src/shared/resolve.ts +33 -0
  27. package/server/src/shared/semver.ts +33 -0
  28. package/server/src/shared/serialize.ts +84 -0
  29. package/server/src/shared/token.ts +59 -0
  30. package/server/src/shared/types.ts +25 -0
  31. package/server/src/shared/utils.ts +17 -0
  32. package/server/tsconfig.json +27 -0
  33. package/src/config.compose.ts +59 -0
  34. package/src/config.consts.ts +5 -0
  35. package/src/config.instance.ts +26 -0
  36. package/src/config.options.ts +58 -0
  37. package/src/config.service.ts +121 -0
  38. package/src/config.types.ts +3 -0
  39. 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,5 @@
1
+ import pkg from "../package.json" with { type: "json" }
2
+
3
+ export const CONFIG_IMAGE_REPO = "localhost/atomservice/config-server"
4
+ export const CONFIG_DEFAULT_PORT = 4921
5
+ export const CONFIG_VERSION = pkg.version
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export type ConfigState = {
2
+ version: string
3
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { ConfigInstance } from "./config.instance.ts"
2
+ export type { ConfigOptions } from "./config.options.ts"
3
+ export { config } from "./config.service.ts"