feat: bind each node to the binary it runs at its first handshake; refuse changes that are not in a CI-signed manifest [CI]
ci/woodpecker/push/main-amd64-build Pipeline failed
ci/woodpecker/push/main-amd64-build Pipeline failed
This commit is contained in:
1 parent
117d34a2af
commit
4841f7b15b
40 files changed
+1011
-21
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Download, KeyRound, Plus, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { api } from '@shared/api/client'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, CapabilityCatalogDto, MeDto, PasskeyDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, AuditDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, NodeBinaryDto, CapabilityCatalogDto, MeDto, PasskeyDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, AuditDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import { fmtBuiltAt, frontendBuild } from '@shared/buildInfo'
|
||||
import { fmtNum, fmtTime } from '@shared/format'
|
||||
import { translateSetting } from '@shared/i18n'
|
||||
@@ -191,6 +191,7 @@ function BuildRow({ name, kind, version, tag, commit, builtAt, platform, update
|
||||
<td className="muted">{fmtBuiltAt(builtAt) || dash}</td>
|
||||
<td className="muted">{platform || dash}</td>
|
||||
<td>{update ?? dash}</td>
|
||||
<td>{dash}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -205,6 +206,26 @@ const UPDATE_STATE: Record<string, { text: string; tone: '' | 'ok' | 'warn' | 'b
|
||||
invalid: { text: '清单无效', tone: 'bad' },
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点二进制的绑定:第一次握手成功时绑定它正在运行的二进制的哈希,之后对不上又不在 CI 签名的更新清单里就拒绝连接。
|
||||
* 「已签名」表示哈希出现在一份 CI 签名的清单里;否则只是首次握手时记下的。不符时可以确认当前的二进制是自己要的(比如手工升级了)。
|
||||
*/
|
||||
function BinaryCell({ binary, busy, onAccept }: { binary?: NodeBinaryDto | null; busy: boolean; onAccept: () => void }) {
|
||||
if (!binary) return <span className="faint">未绑定</span>
|
||||
const short = binary.sha256.slice(0, 10)
|
||||
return (
|
||||
<span className="row" style={{ gap: 8 }} title={binary.violation ?? binary.sha256}>
|
||||
{binary.violation ? <Chip tone="bad">不符</Chip> : <Chip tone={binary.signed ? 'ok' : ''}>{binary.signed ? '已签名' : '首次绑定'}</Chip>}
|
||||
<span className="mono muted">{short}</span>
|
||||
{binary.violation ? (
|
||||
<button type="button" className="btn small" disabled={busy} onClick={onAccept} title="清除绑定,节点下一次连接时重新绑定当前的二进制">
|
||||
接受当前二进制
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function UpdateCell({ update, busy, onUpdate }: { update?: NodeUpdateDto | null; busy: boolean; onUpdate: () => void }) {
|
||||
if (!update) return <span className="faint">—</span>
|
||||
const s = UPDATE_STATE[update.state] ?? { text: update.state, tone: '' as const }
|
||||
@@ -271,6 +292,7 @@ function Versions() {
|
||||
<th>构建时间</th>
|
||||
<th>系统</th>
|
||||
<th>更新</th>
|
||||
<th>二进制</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -292,6 +314,9 @@ function Versions() {
|
||||
<td>
|
||||
<UpdateCell update={n.update} busy={busy} onUpdate={() => requestUpdate([n.id])} />
|
||||
</td>
|
||||
<td>
|
||||
<BinaryCell binary={n.binary} busy={busy} onAccept={() => void run(async () => { await api.del(`/api/admin/nodes/${n.id}/binary`); info.reload() }, '已清除绑定,节点下一次连接时重新绑定')} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -988,6 +988,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/nodes/{id}/binary": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
/** Accept the node's current binary: clear its binding so the next handshake binds it again */
|
||||
delete: operations["nodes_binary_clear"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/backend/update": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1680,6 +1697,17 @@ export interface components {
|
||||
version: number | string;
|
||||
needs: string[];
|
||||
};
|
||||
NodeBinaryDto: {
|
||||
sha256: string;
|
||||
signed: boolean;
|
||||
/** Format: int64 */
|
||||
seq: number | string;
|
||||
/** Format: date-time */
|
||||
bound_at: unknown;
|
||||
violation: null | string;
|
||||
/** Format: date-time */
|
||||
violation_at: unknown;
|
||||
};
|
||||
NodeBuildDto: {
|
||||
id: string;
|
||||
kind: string;
|
||||
@@ -1691,6 +1719,7 @@ export interface components {
|
||||
commit: null | string;
|
||||
built_at: null | string;
|
||||
update?: null | components["schemas"]["NodeUpdateDto"];
|
||||
binary?: null | components["schemas"]["NodeBinaryDto"];
|
||||
};
|
||||
NodeInfoDto: {
|
||||
id: string;
|
||||
@@ -5593,6 +5622,46 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
nodes_binary_clear: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
backend_update_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -19,6 +19,7 @@ export type BuildInfoDto = Narrow<S['BuildInfoDto']>
|
||||
export type BackendUpdateDto = Narrow<S['BackendUpdateDto']>
|
||||
export type NodeBuildDto = Narrow<S['NodeBuildDto']>
|
||||
export type NodeUpdateDto = Narrow<S['NodeUpdateDto']>
|
||||
export type NodeBinaryDto = Narrow<S['NodeBinaryDto']>
|
||||
export type ReleaseDto = Narrow<S['ReleaseDto']>
|
||||
export type LinkDto = Narrow<S['LinkDto']>
|
||||
export type LinkMatrixDto = Narrow<S['LinkMatrixDto']>
|
||||
|
||||
Reference in new issue
Block a user