@atomservice/app 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 +36 -0
- package/src/app.compose.ts +30 -0
- package/src/app.options.ts +20 -0
- package/src/app.service.ts +89 -0
- package/src/index.ts +2 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomservice/app",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "通用应用容器原子服务",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "openorson",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/openorson/atomservice.git",
|
|
10
|
+
"directory": "services/app"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/openorson/atomservice/issues",
|
|
13
|
+
"homepage": "https://github.com/openorson/atomservice/tree/main/services/app#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"atomservice",
|
|
16
|
+
"container",
|
|
17
|
+
"app",
|
|
18
|
+
"self-hosted",
|
|
19
|
+
"podman"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.3.14"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@atomservice/gateway": "0.1.5"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@atomservice/core": "0.1.5"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { tmpl } from "@atomservice/core"
|
|
2
|
+
|
|
3
|
+
export function appCompose(
|
|
4
|
+
id: string,
|
|
5
|
+
contextPath: string,
|
|
6
|
+
containerfilePath: string | undefined,
|
|
7
|
+
port: number,
|
|
8
|
+
hostPort?: number,
|
|
9
|
+
): string {
|
|
10
|
+
const portsBlock = hostPort ? `\n ports:\n - "${hostPort}:${port}"` : ""
|
|
11
|
+
const dockerfileBlock = containerfilePath ? `\n dockerfile: ${containerfilePath}` : ""
|
|
12
|
+
|
|
13
|
+
return tmpl`\
|
|
14
|
+
services:
|
|
15
|
+
${id}:
|
|
16
|
+
build:
|
|
17
|
+
context: ${contextPath}${dockerfileBlock}
|
|
18
|
+
container_name: ${id}
|
|
19
|
+
expose:
|
|
20
|
+
- "${port}"${portsBlock}
|
|
21
|
+
networks:
|
|
22
|
+
- atomservice
|
|
23
|
+
restart: unless-stopped
|
|
24
|
+
|
|
25
|
+
networks:
|
|
26
|
+
atomservice:
|
|
27
|
+
name: atomservice
|
|
28
|
+
external: true
|
|
29
|
+
`
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CallableService } from "@atomservice/core"
|
|
2
|
+
import type { GatewayInstance } from "@atomservice/gateway"
|
|
3
|
+
|
|
4
|
+
export type GitConfig = boolean | { branch?: string }
|
|
5
|
+
|
|
6
|
+
export interface ProjectConfig {
|
|
7
|
+
root: { path: string }
|
|
8
|
+
package?: { path: string }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AppOptions {
|
|
12
|
+
id?: string
|
|
13
|
+
project: ProjectConfig
|
|
14
|
+
git?: GitConfig
|
|
15
|
+
port?: number
|
|
16
|
+
hostPort?: number
|
|
17
|
+
healthPath?: string
|
|
18
|
+
domain?: string
|
|
19
|
+
gateway?: CallableService<GatewayInstance>
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import type { CallableService } from "@atomservice/core"
|
|
3
|
+
import { defineService, onDown, onHealth, onUp, useConfig, useLogger, useShell } from "@atomservice/core"
|
|
4
|
+
import { appCompose } from "./app.compose.ts"
|
|
5
|
+
import type { AppOptions, GitConfig } from "./app.options.ts"
|
|
6
|
+
|
|
7
|
+
export function app(opts: AppOptions): CallableService {
|
|
8
|
+
const serviceId = opts.id && opts.id !== "default" ? `app-${opts.id}` : "app"
|
|
9
|
+
const port = opts.port ?? 3000
|
|
10
|
+
|
|
11
|
+
return defineService({
|
|
12
|
+
name: "app",
|
|
13
|
+
id: opts.id,
|
|
14
|
+
setup: () => {
|
|
15
|
+
const logger = useLogger()
|
|
16
|
+
const $ = useShell()
|
|
17
|
+
const config = useConfig()
|
|
18
|
+
const gw = opts.gateway?.()
|
|
19
|
+
|
|
20
|
+
const composePath = path.join(config.root, serviceId, "compose.yml")
|
|
21
|
+
|
|
22
|
+
function resolveProjectPaths() {
|
|
23
|
+
const rootPath = opts.project.root.path
|
|
24
|
+
const pkg = opts.project.package
|
|
25
|
+
const packagePath = pkg ? (pkg.path.startsWith("/") ? pkg.path : path.join(rootPath, pkg.path)) : undefined
|
|
26
|
+
const containerfilePath = packagePath ? path.join(packagePath, "Containerfile") : undefined
|
|
27
|
+
return { rootPath, containerfilePath }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveGitBranch(git: GitConfig | undefined): string | null {
|
|
31
|
+
if (!git) return null
|
|
32
|
+
if (git === true) return "main"
|
|
33
|
+
return git.branch ?? "main"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onUp(async () => {
|
|
37
|
+
const { rootPath, containerfilePath } = resolveProjectPaths()
|
|
38
|
+
const branch = resolveGitBranch(opts.git)
|
|
39
|
+
|
|
40
|
+
if (branch) {
|
|
41
|
+
logger.info(`拉取代码 ${branch}…`)
|
|
42
|
+
await $`git -C ${rootPath} pull origin ${branch}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await $`mkdir -p ${path.join(config.root, serviceId)}`
|
|
46
|
+
await Bun.write(composePath, appCompose(serviceId, rootPath, containerfilePath, port, opts.hostPort))
|
|
47
|
+
|
|
48
|
+
logger.info(`构建并启动 ${serviceId}…`)
|
|
49
|
+
await $`podman compose -f ${composePath} up -d --build`
|
|
50
|
+
|
|
51
|
+
if (opts.domain && gw) {
|
|
52
|
+
logger.info(`注册路由 ${opts.domain} → ${serviceId}:${port}`)
|
|
53
|
+
await gw.registerRoute(opts.domain, `${serviceId}:${port}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.success(`${serviceId} 已启动`)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
onDown(async () => {
|
|
60
|
+
logger.info(`停止 ${serviceId}…`)
|
|
61
|
+
await $`podman compose -f ${composePath} down`
|
|
62
|
+
|
|
63
|
+
if (opts.domain && gw) {
|
|
64
|
+
await gw.unregisterRoute(opts.domain)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.success(`${serviceId} 已停止`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
onHealth(async () => {
|
|
71
|
+
if (opts.hostPort) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`http://localhost:${opts.hostPort}${opts.healthPath ?? "/"}`)
|
|
74
|
+
if (res.ok || res.status < 500) return { status: "healthy" }
|
|
75
|
+
return { status: "unhealthy", message: `HTTP ${res.status}` }
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { status: "unhealthy", message: String(e) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await $`podman inspect --format {{.State.Status}} ${serviceId}`.quiet().nothrow()
|
|
82
|
+
const containerStatus = result.text().trim()
|
|
83
|
+
return containerStatus === "running"
|
|
84
|
+
? { status: "healthy" }
|
|
85
|
+
: { status: "unhealthy", message: `容器状态: ${containerStatus}` }
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
}
|
package/src/index.ts
ADDED