feat: install nodes and the backend from a static compose file; remove dead code [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Agent, Probe and Backend now start from one static compose.yaml plus a local
.env: the panel (or the release assets, for the very first Backend) provides
the file, the panel's command writes the public values into .env, checks the
file's SHA-256 and runs compose up. Updating is compose pull && compose up -d.
The image is chosen by the CI channel (:latest, or MOSHIRO_VERSION in .env),
not by the panel; the previous one-line script stays as a fallback.

- packaging/compose/{agent,probe,backend}/compose.yaml are the single source,
  embedded in the Backend and served at /install/compose/{file}; the root
  compose.yaml moved there. Releases also carry them and list them in the
  signed SHA256SUMS.
- The container entrypoint defaults the node name to the host name.
- Join token creation returns a compose block (.env, url, hash, per-platform
  commands); the admin dialog leads with it.
- Remove code with no references (HotLayer members, LiveHub.SubscriberCount,
  QueryService.ParseParams, ReleaseService.Forget, a few constants) and reduce
  the install-code lookup to the role it is for.
This commit is contained in:
justme committed 2026-10-08 20:24:23 +08:00
1 parent e014a19554
commit 12be44645f
31 files changed
+492 -176

No files matched your search

+52 -16
View File
@@ -1,13 +1,21 @@
import type { CreatedJoinTokenDto } from '@shared/api/types'
import { fmtTime } from '@shared/format'
import { CommandTabs, Dialog, Field, guessPlatform } from '@shared/ui'
import { CommandTabs, CopyButton, Dialog, Field, guessPlatform, type CommandTab } from '@shared/ui'
/**
* 加入令牌创建之后的对话框:Agent 和 Probe 共用同一份,一行命令装好。
* 命令里只有一次性代码,角色由代码决定;节点名字默认取主机名。
* 加入令牌创建之后的对话框:Agent 和 Probe 共用同一份。
* 首选 compose:把面板给的 .env 存下来,下载 compose.yaml,`compose up -d`,以后更新就是 `compose pull && compose up -d`。
* .env 里只有公开的值(面板地址、一次性码、信任根指纹和属主),没有任何私钥;脚本一行命令留作备用。
*/
export function JoinTokenCreated({ created, kind, onClose }: { created: CreatedJoinTokenDto; kind: 'Agent' | 'Probe'; onClose: () => void }) {
const commands = created.install_commands
const compose = created.compose
const script = created.install_commands
const composeTabs: CommandTab[] = [
{ id: 'linux', label: 'Linux · Podman', command: compose.linux },
...(compose.macos ? [{ id: 'macos', label: 'macOS · Docker', command: compose.macos }] : []),
...(compose.windows ? [{ id: 'windows', label: 'Windows · Docker', command: compose.windows }] : []),
]
const initial = composeTabs.some((t) => t.id === guessPlatform()) ? guessPlatform() : 'linux'
return (
<Dialog
title="加入令牌已创建"
@@ -19,25 +27,53 @@ export function JoinTokenCreated({ created, kind, onClose }: { created: CreatedJ
}
>
<div className="stack">
<div className="notice warn">命令里的代码只显示这一次,请现在就复制。命令里只有公开的值(面板地址、一次性码、信任根指纹和属主),没有任何私钥。</div>
<div className="notice warn">一次性码只显示这一次,请现在就复制。下面只有公开的值(面板地址、一次性码、信任根指纹和属主),没有任何私钥。</div>
<Field
label={`容器安装(推荐):在要加入的机器上运行(${kind})`}
hint="需要 Docker 或 Podman。命令会先下载安装脚本并核对它的 SHA-256,不一致就不执行;节点名字默认取主机名;身份和配置存在命名卷里,重启、换新镜像重建容器都不用再给参数。名字想自己定,或者提示名字被占用,在命令里加上 MOSHIRO_NAME=名字(Linux 和 macOS 写在 bash 或 zsh 前面,Windows 先执行 $env:MOSHIRO_NAME='名字')。"
hint="需要 podman compose 或 docker compose。命令建一个目录,把参数写进 .env(权限 600),下载 compose.yaml 并核对 SHA-256,一致才启动。节点名字默认取主机名,想自己定就改 .env 里的 MOSHIRO_NAME。"
>
<CommandTabs
storageKey="moshiro.install_tab"
initial={guessPlatform()}
tabs={[
{ id: 'linux', label: 'Linux · 容器', command: commands.linux },
{ id: 'macos', label: 'macOS · 容器', command: commands.macos },
{ id: 'windows', label: 'Windows · 容器', command: commands.windows },
]}
/>
<CommandTabs storageKey="moshiro.compose_tab" initial={initial} tabs={composeTabs} />
</Field>
<details>
<summary>分步做:.env 内容、compose 下载地址和哈希</summary>
<div className="stack" style={{ marginTop: 8 }}>
<Field label=".env(存成当前目录下的 .env)">
<div className="row" style={{ alignItems: 'flex-start', gap: 8 }}>
<pre className="code" style={{ flex: 1, whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{compose.env}</pre>
<CopyButton text={compose.env} />
</div>
</Field>
<Field label="compose.yaml">
<div className="stack">
<div className="row" style={{ gap: 8 }}>
<span className="mono" style={{ wordBreak: 'break-all' }}>{compose.url}</span>
<CopyButton text={compose.url} />
</div>
<div className="muted mono" style={{ wordBreak: 'break-all' }}>SHA-256 {compose.sha256}</div>
</div>
</Field>
</div>
</details>
<p className="muted">
容器方式是首选:镜像按 CI 签名的清单固定到 digest,宿主机更新器能自己更新它,属主和信任根在注册时固定。{kind === 'Agent' ? (created.token.auto_approve ? '使用它注册的 Agent 会自动启用。' : '使用它注册的 Agent 需要你在列表里审批之后才会启用。') : ''}
以后更新:<span className="mono">podman compose pull &amp;&amp; podman compose up -d</span>(Docker 把 podman 换成 docker);身份和配置在命名卷里,不会动。
想固定版本,在 .env 里写 <span className="mono">MOSHIRO_VERSION=版本号</span>。注册成功之后,可以把 .env 里的 <span className="mono">MOSHIRO_JOIN</span> 一行删掉。
{kind === 'Agent' ? (created.token.auto_approve ? ' 使用它注册的 Agent 会自动启用。' : ' 使用它注册的 Agent 需要你在列表里审批之后才会启用。') : ''}
可用 {created.token.max_uses} 次,{fmtTime(created.token.expires_at)} 过期。
</p>
<details>
<summary>备用:一行安装脚本(先核对脚本哈希再执行,需要 Docker 或 Podman)</summary>
<div style={{ marginTop: 8 }}>
<CommandTabs
storageKey="moshiro.install_tab"
initial={guessPlatform()}
tabs={[
{ id: 'linux', label: 'Linux · 脚本', command: script.linux },
{ id: 'macos', label: 'macOS · 脚本', command: script.macos },
{ id: 'windows', label: 'Windows · 脚本', command: script.windows },
]}
/>
</div>
</details>
</div>
</Dialog>
)
+55
View File
@@ -1178,6 +1178,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/install/compose/{file}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The static compose file of agent, probe or backend (agent.yaml, probe.yaml, backend.yaml; same bytes for everyone) */
get: operations["install_compose"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/install.ps1": {
parameters: {
query?: never;
@@ -1407,6 +1424,14 @@ export interface components {
[key: string]: null | number | string;
};
};
ComposeInstallDto: {
env: string;
url: string;
sha256: string;
linux: string;
macos: string;
windows: string;
};
ConfirmBackendUpdateRequest: {
/** Format: int64 */
seq: number | string;
@@ -1459,6 +1484,7 @@ export interface components {
token: components["schemas"]["JoinTokenDto"];
plaintext: string;
install_commands: components["schemas"]["InstallCommandsDto"];
compose: components["schemas"]["ComposeInstallDto"];
};
DimensionDto: {
id: string;
@@ -6179,6 +6205,35 @@ export interface operations {
};
};
};
install_compose: {
parameters: {
query?: never;
header?: never;
path: {
file: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
install_ps1: {
parameters: {
query?: never;
+1
View File
@@ -40,6 +40,7 @@ export type ParamDto = Narrow<S['ParamDto']>
export type MetricDto = Narrow<S['MetricDto']>
export type JoinTokenDto = Narrow<S['JoinTokenDto']>
export type CreatedJoinTokenDto = Narrow<S['CreatedJoinTokenDto']>
export type ComposeInstallDto = Narrow<S['ComposeInstallDto']>
export type ApiTokenDto = Narrow<S['ApiTokenDto']>
export type CreatedApiTokenDto = Narrow<S['CreatedApiTokenDto']>
export type CapabilityCatalogDto = Narrow<S['CapabilityCatalogDto']>