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>
|
||||
|
||||
Reference in new issue
Block a user