Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- 槽位种子页默认锁定:需确认解锁后才能改配置,播种前再次确认;时区/语言提供 en、cn、jp 基础选项。时区跟随当前 SOCKS5(空/非手动钉住)时展示出口时区,播种经 `timezone_follow_proxy` 写入;可改固定 IANA 时区或改回跟随。TZ/LANG/LC_ALL 仍不经 `extra_env` 覆盖契约键。
- 槽位种子页:`settings.json` 的 env 行与 `theme` 可直接编辑并随播种写入;`autoUpdates`、`grove_enabled` 只读且播种时固定为 `false`;TZ/LANG/LC_ALL、遥测 kill-switch、Bedrock/Vertex、gateway hint 等同理只读(播种由契约重写)。缺 `CLAUDE_CODE_ATTRIBUTION_HEADER` 时默认 `0`。调试上传默认关,独立 query 拉最近 40 条上传,切换开关不重置未保存编辑;Go sidecar 写 `vms/<id>/run/telemetry-upload.log`,面板 `GET seed-settings?debug=1` 返回 `telemetry_log`。
- 槽位种子页按当前槽 `settings.json` 列出真实配置;`claude.setting` 可编辑播种;缺失文件返回 `null`、空文件返回空串,不用策略默认冒充槽内文件;遥测开关与 sidecar 进程状态分开展示,保存不因遥测进程刷新失败而 502。
- 集群槽 legacy kin-worker / kin-egress 容器可一次性迁到持久化 `/opt/kin-live` 挂载:旧容器重命名为 `*-legacy-<timestamp>` 保留作回滚,替换容器复用原镜像与网络身份;rename 后先从 user-defined 网 disconnect 释放 IP/别名再 create;KVM 已挂 `/opt/kin-guest/opt/kin-live` 不再误判 legacy;`scripts/migrate-legacy-cluster-slots.mjs` 与 fleet `action=telemetry` 同路径;迁移时保留 CapDrop/CapAdd 等 HostConfig,避免 canReuse 后 egress 权限缩水。(#605)
- fleet 遥测刷新在 kin-egress 二进制被替换(含 `(deleted)` 旧进程与脏 pidfile)时,会按 egress 配置扫停全部 helper、等待 `/proc/<pid>` 消失后再绑定新二进制;同一 SOCKS 出口的 stop/start 与 `ensureProxyEgress` 共用 per-proxy 锁,避免并发 telemetry 互相 SIGTERM。(#604)
- 面板:侧栏「GitHub / 文档」两枚链接不再折行,字体偏宽时「文档」不会逐字竖排,空间不够先缩箭头再省略文字;VM 列表搜索框关闭浏览器自动填充,不再被回填成登录邮箱。
- 集群页「扩展节点」新增 CPU / 内存 / 容器三列:健康检查(每 30 秒)经同一条 SSH 连接读节点 `/proc` 和 cgroup,口径同本机宿主列;容器 = 节点上全部 Docker 容器内存合计(扣除 inactive_file,同 `docker stats`)。修复本机宿主列 CPU 最高只显示 10%(`Math.min(100, …×1000)/10` 写错上限)。链路列不再折行。
Expand Down
10 changes: 10 additions & 0 deletions docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ reporting success. A failed service restart is reported per slot so the old VM
identity remains intact and can be rolled back by republishing the previous
binary.

For a controlled rollout, run `scripts/migrate-legacy-cluster-slots.mjs` in
the control-plane container. Pass VM IDs after the root and concurrency
arguments to limit the rollout. It performs the same migration used by the
one-click installer: legacy slot and egress containers are stopped, renamed to
`*-legacy-<timestamp>`, and kept as stopped rollback copies; replacement
containers use the persistent `/opt/kin-live` mount. Container IDs therefore
change, but the VM, credentials, `vms/`, routing, and network identity do not.
After this one-time migration, subsequent updates atomically replace only the
Go binaries in `slot-bin` and restart the service processes.

## Upload audit log (debug)

Each successful or failed `PostBatch` / GrowthBook eval append one line to
Expand Down
24 changes: 24 additions & 0 deletions scripts/migrate-legacy-cluster-slots.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env node

/**
* Migrate legacy cluster slot containers to the persistent Go-service mount.
* This never rebuilds a VM: the old container is renamed and retained as a
* stopped rollback copy, while the replacement reuses its image and settings.
*/
import { runFleetUpdate } from '../src/lib/vm/fleet-update.mjs'

const root = process.argv[2] || process.env.KIN_PROJECT_ROOT || '/opt/vm2api'
const concurrency = Number(process.argv[3] || process.env.KIN_FLEET_CONCURRENCY || 4)
const ids = process.argv
.slice(4)
.flatMap((value) => String(value).split(','))
.map((value) => value.trim())
.filter(Boolean)

const report = await runFleetUpdate(root, {
action: 'telemetry',
concurrency,
ids: ids.length ? ids : null,
})
console.log(JSON.stringify(report))
if (report.failed_count) process.exitCode = 1
6 changes: 5 additions & 1 deletion scripts/panel-upgrade.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ while [ "$attempt" -lt 45 ]; do
if probe verify "$TAG" "$BASELINE"; then
[ -d .git ] || printf '%s\n' "${TAG#v}" > VERSION
# Replace the Go worker/egress payloads in-place; no VM/container rebuild.
if ! docker exec "$CONTAINER" node /opt/vm2api/scripts/fleet-services.mjs; then
# Older control-plane images may not carry the helper yet; retry on the
# next image update instead of invalidating an otherwise verified upgrade.
if ! docker exec "$CONTAINER" test -f /opt/vm2api/scripts/fleet-services.mjs >/dev/null 2>&1; then
echo "Go service refresh helper is not present in the control plane; retry after the next image update" >&2
elif ! docker exec "$CONTAINER" node /opt/vm2api/scripts/fleet-services.mjs; then
echo "Go service-only update failed; inspect the per-slot report" >&2
exit 1
fi
Expand Down
38 changes: 38 additions & 0 deletions src/lib/cluster/docker-remote.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,16 @@ export async function removeContainer(connect, id) {
await dockerJson(connect, { method: 'DELETE', path: `${containerPath(id)}?force=1&v=1`, timeoutMs: 60_000 }, [204])
}

/** Preserve the container object for a rollback instead of deleting it. */
export async function renameContainer(connect, id, name) {
if (!NAME_RE.test(String(name || ''))) throw new ClusterError(400, 'invalid_name', '容器名不合法')
await dockerJson(
connect,
{ method: 'POST', path: `${containerPath(id)}/rename?name=${encodeURIComponent(name)}`, timeoutMs: 30_000 },
[204],
)
}

export async function containerLogs(connect, id, { tail = 200 } = {}) {
const n = Math.min(5000, Math.max(1, Number.parseInt(tail, 10) || 200))
const inspect = await dockerJson(connect, { path: `${containerPath(id)}/json` })
Expand Down Expand Up @@ -502,6 +512,34 @@ export async function removeNetwork(connect, name) {
throw new ClusterError(502, 'docker_network_failed', dockerMessage(res))
}

/** Detach a container so its static IP / aliases are free for a replacement join. */
export async function disconnectContainerFromNetwork(connect, network, container) {
await dockerJson(
connect,
{
method: 'POST',
path: `/networks/${encodeURIComponent(network)}/disconnect`,
body: { Container: container },
timeoutMs: 30_000,
},
[200, 204, 404],
)
}

/** Re-attach a container with the endpoint it had before a failed migration rollback. */
export async function connectContainerToNetwork(connect, network, container, endpointConfig = {}) {
await dockerJson(
connect,
{
method: 'POST',
path: `/networks/${encodeURIComponent(network)}/connect`,
body: { Container: container, EndpointConfig: endpointConfig },
timeoutMs: 30_000,
},
[200, 204],
)
}

/**
* POST /build with a (gzip) tar context stream. Progress is NDJSON; a failure
* after the 200 header only shows up as an `error` line.
Expand Down
Loading
Loading