Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 4 additions & 3 deletions docs/k3s-update-center.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,9 @@ helper 端使用:

补充一点:

- `Docker Hub` 里的**自动发现**仍然优先 `latest` / `main` / 稳定 SemVer 标签
- 如果你要部署 `dev`、分支名、临时标签或 `sha-*` 这类非稳定标签,直接在 Docker Hub 卡片底部用“手动部署 Docker Hub 标签”填写即可
- `Docker Hub` 里的**主候选**仍然优先 `latest` / `main` / 稳定 SemVer 标签
- 页面还会自动列出最近推送的 `dev`、分支名、临时标签或 `sha-*` 这类**非稳定标签**,并一起带出 digest
- 如果你要部署一个更老或更特殊、没出现在自动列表里的标签,仍然可以在 Docker Hub 卡片底部用“手动部署 Docker Hub 标签”填写

对已经跑起来的 K3s / Helm 用户来说,更新中心的日常配置主要就在这一页:

Expand All @@ -330,7 +331,7 @@ helper 端使用:
- 版本来源发现了可部署版本
- 如果 Docker Hub 显示的是 `latest @ sha256:...`,说明页面已经识别到 alias tag 当前指向的具体镜像 digest
- Deploy Helper 显示健康
4. 如果你要跟随稳定候选,直接点部署按钮;如果你要切到 `dev` / 分支 / 临时标签,就在 Docker Hub 卡片底部手动填写 tag,必要时连 digest 一起填
4. 如果你要跟随稳定候选,直接点部署按钮;如果你要切到最近的 `dev` / 分支 / 临时标签,可以直接点自动列出的那一项;只有更特殊的 tag 才需要手动填写,必要时连 digest 一起填
5. 在页面下方看部署日志
6. 如果升级后发现问题,可以直接在“回退历史”里点旧 revision 回滚;只要该 revision 当时记录了 digest,就会跟着一起回到对应镜像

Expand Down
110 changes: 89 additions & 21 deletions src/server/routes/api/updateCenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { waitForBackgroundTaskToReachTerminalState } from '../../test-fixtures/b

const {
fetchLatestStableGitHubReleaseMock,
fetchLatestDockerHubTagMock,
fetchDockerHubTagCandidatesMock,
getUpdateCenterHelperStatusMock,
streamUpdateCenterDeployMock,
streamUpdateCenterRollbackMock,
} = vi.hoisted(() => ({
fetchLatestStableGitHubReleaseMock: vi.fn(),
fetchLatestDockerHubTagMock: vi.fn(),
fetchDockerHubTagCandidatesMock: vi.fn(),
getUpdateCenterHelperStatusMock: vi.fn(),
streamUpdateCenterDeployMock: vi.fn(),
streamUpdateCenterRollbackMock: vi.fn(),
Expand All @@ -26,7 +26,7 @@ vi.mock('../../services/updateCenterVersionService.js', async () => {
return {
...actual,
fetchLatestStableGitHubRelease: (...args: unknown[]) => fetchLatestStableGitHubReleaseMock(...args),
fetchLatestDockerHubTag: (...args: unknown[]) => fetchLatestDockerHubTagMock(...args),
fetchDockerHubTagCandidates: (...args: unknown[]) => fetchDockerHubTagCandidatesMock(...args),
};
});

Expand Down Expand Up @@ -103,7 +103,7 @@ describe('update center routes', () => {

beforeEach(async () => {
fetchLatestStableGitHubReleaseMock.mockReset();
fetchLatestDockerHubTagMock.mockReset();
fetchDockerHubTagCandidatesMock.mockReset();
getUpdateCenterHelperStatusMock.mockReset();
streamUpdateCenterDeployMock.mockReset();
streamUpdateCenterRollbackMock.mockReset();
Expand Down Expand Up @@ -139,6 +139,18 @@ describe('update center routes', () => {
publishedAt: '2026-03-29T11:54:35.591877Z',
url: null,
} as const;
const dockerHubRecentTags = [
{
source: 'docker-hub-tag',
rawVersion: 'dev',
normalizedVersion: 'dev',
tagName: 'dev',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
publishedAt: '2026-03-29T12:54:35.591877Z',
url: null,
},
] as const;
const helperStatus = {
ok: true,
releaseName: 'metapi',
Expand All @@ -161,7 +173,10 @@ describe('update center routes', () => {
],
} as const;
fetchLatestStableGitHubReleaseMock.mockResolvedValue(githubRelease);
fetchLatestDockerHubTagMock.mockResolvedValue(dockerHubTag);
fetchDockerHubTagCandidatesMock.mockResolvedValue({
primary: dockerHubTag,
recentNonStable: dockerHubRecentTags,
});
getUpdateCenterHelperStatusMock.mockResolvedValue(helperStatus);

const saveResponse = await app.inject({
Expand Down Expand Up @@ -216,6 +231,13 @@ describe('update center routes', () => {
displayVersion: 'latest @ sha256:efb2ee655386',
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
},
dockerHubRecentTags: [
{
normalizedVersion: 'dev',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
],
helper: {
ok: true,
healthy: true,
Expand All @@ -242,11 +264,14 @@ describe('update center routes', () => {

it('returns partial status when a single version source lookup fails', async () => {
fetchLatestStableGitHubReleaseMock.mockRejectedValue(new Error('GitHub releases lookup timed out'));
fetchLatestDockerHubTagMock.mockResolvedValue({
source: 'docker-hub-tag',
rawVersion: '1.3.1',
normalizedVersion: '1.3.1',
url: null,
fetchDockerHubTagCandidatesMock.mockResolvedValue({
primary: {
source: 'docker-hub-tag',
rawVersion: '1.3.1',
normalizedVersion: '1.3.1',
url: null,
},
recentNonStable: [],
});
getUpdateCenterHelperStatusMock.mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -426,6 +451,18 @@ describe('update center routes', () => {
publishedAt: '2026-03-31T09:00:00Z',
url: null,
},
dockerHubRecentTags: [
{
source: 'docker-hub-tag',
rawVersion: 'dev',
normalizedVersion: 'dev',
tagName: 'dev',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
publishedAt: '2026-03-31T09:05:00Z',
url: null,
},
],
helper: {
ok: true,
releaseName: 'metapi',
Expand Down Expand Up @@ -454,6 +491,12 @@ describe('update center routes', () => {
dockerHubTag: {
displayVersion: 'latest @ sha256:efb2ee655386',
},
dockerHubRecentTags: [
{
normalizedVersion: 'dev',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
},
],
helper: {
imageDigest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
},
Expand All @@ -462,7 +505,7 @@ describe('update center routes', () => {
},
});
expect(fetchLatestStableGitHubReleaseMock).not.toHaveBeenCalled();
expect(fetchLatestDockerHubTagMock).not.toHaveBeenCalled();
expect(fetchDockerHubTagCandidatesMock).not.toHaveBeenCalled();
expect(getUpdateCenterHelperStatusMock).not.toHaveBeenCalled();
});

Expand All @@ -477,15 +520,29 @@ describe('update center routes', () => {
publishedAt: '2026-03-31T10:00:00Z',
url: 'https://github.com/cita-777/metapi/releases/tag/v1.3.1',
});
fetchLatestDockerHubTagMock.mockResolvedValue({
source: 'docker-hub-tag',
rawVersion: 'latest',
normalizedVersion: 'latest',
tagName: 'latest',
digest: 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
displayVersion: 'latest @ sha256:dddddddddddd',
publishedAt: '2026-03-31T10:00:00Z',
url: null,
fetchDockerHubTagCandidatesMock.mockResolvedValue({
primary: {
source: 'docker-hub-tag',
rawVersion: 'latest',
normalizedVersion: 'latest',
tagName: 'latest',
digest: 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
displayVersion: 'latest @ sha256:dddddddddddd',
publishedAt: '2026-03-31T10:00:00Z',
url: null,
},
recentNonStable: [
{
source: 'docker-hub-tag',
rawVersion: 'dev-20260417-f67ade2',
normalizedVersion: 'dev-20260417-f67ade2',
tagName: 'dev-20260417-f67ade2',
digest: 'sha256:abababababababababababababababababababababababababababababababab',
displayVersion: 'dev-20260417-f67ade2 @ sha256:abababababab',
publishedAt: '2026-03-31T10:05:00Z',
url: null,
},
],
});
getUpdateCenterHelperStatusMock.mockResolvedValue({
ok: true,
Expand All @@ -512,6 +569,12 @@ describe('update center routes', () => {
dockerHubTag: {
digest: 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
},
dockerHubRecentTags: [
{
normalizedVersion: 'dev-20260417-f67ade2',
digest: 'sha256:abababababababababababababababababababababababababababababababab',
},
],
helper: {
revision: '13',
},
Expand All @@ -520,7 +583,7 @@ describe('update center routes', () => {
},
});
expect(fetchLatestStableGitHubReleaseMock).toHaveBeenCalledTimes(1);
expect(fetchLatestDockerHubTagMock).toHaveBeenCalledTimes(1);
expect(fetchDockerHubTagCandidatesMock).toHaveBeenCalledTimes(1);
expect(getUpdateCenterHelperStatusMock).toHaveBeenCalledTimes(1);
expect(await loadUpdateCenterRuntimeState()).toEqual(expect.objectContaining({
lastResolvedSource: 'github-release',
Expand All @@ -532,6 +595,11 @@ describe('update center routes', () => {
dockerHubTag: expect.objectContaining({
digest: 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
}),
dockerHubRecentTags: [
expect.objectContaining({
normalizedVersion: 'dev-20260417-f67ade2',
}),
],
helper: expect.objectContaining({
revision: '13',
}),
Expand Down
14 changes: 10 additions & 4 deletions src/server/services/updateCenterPollingService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('updateCenterPollingService', () => {
publishedAt: '2026-03-31T12:00:00Z',
},
dockerHubTag: null,
dockerHubRecentTags: [],
helper: {
ok: true,
releaseName: 'metapi',
Expand All @@ -115,6 +116,7 @@ describe('updateCenterPollingService', () => {
currentVersion: '1.2.3',
githubRelease: runtime.statusSnapshot.githubRelease,
dockerHubTag: null,
dockerHubRecentTags: [],
helper: runtime.statusSnapshot.helper,
runtime,
},
Expand Down Expand Up @@ -151,15 +153,16 @@ describe('updateCenterPollingService', () => {
lastResolvedSource: 'github-release',
lastResolvedCandidateKey: 'github-release:v1.3.0',
lastNotifiedCandidateKey: 'github-release:v1.3.0',
statusSnapshot: {
statusSnapshot: expect.objectContaining({
githubRelease: expect.objectContaining({
normalizedVersion: '1.3.0',
}),
dockerHubTag: null,
dockerHubRecentTags: [],
helper: expect.objectContaining({
imageTag: '1.2.3',
}),
},
}),
}));

await vi.advanceTimersByTimeAsync(60_000);
Expand Down Expand Up @@ -205,6 +208,7 @@ describe('updateCenterPollingService', () => {
publishedAt: '2026-03-31T12:01:00Z',
},
dockerHubTag: null,
dockerHubRecentTags: [],
helper: {
ok: true,
releaseName: 'metapi',
Expand All @@ -223,6 +227,7 @@ describe('updateCenterPollingService', () => {
currentVersion: '1.2.3',
githubRelease: runtime.statusSnapshot.githubRelease,
dockerHubTag: null,
dockerHubRecentTags: [],
helper: runtime.statusSnapshot.helper,
runtime,
},
Expand Down Expand Up @@ -251,15 +256,16 @@ describe('updateCenterPollingService', () => {
lastResolvedCandidateKey: 'github-release:v1.3.0',
lastNotifiedCandidateKey: 'github-release:v1.3.0',
lastNotifiedAt: expect.any(String),
statusSnapshot: {
statusSnapshot: expect.objectContaining({
githubRelease: expect.objectContaining({
normalizedVersion: '1.3.0',
}),
dockerHubTag: null,
dockerHubRecentTags: [],
helper: expect.objectContaining({
imageTag: '1.2.3',
}),
},
}),
}));
});
});
24 changes: 24 additions & 0 deletions src/server/services/updateCenterRuntimeStateService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ describe('updateCenterRuntimeStateService', () => {
displayVersion: 'latest @ sha256:efb2ee655386',
publishedAt: '2026-03-30T20:30:00Z',
},
dockerHubRecentTags: [
{
source: 'docker-hub-tag',
rawVersion: 'dev',
normalizedVersion: 'dev',
url: null,
tagName: 'dev',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
publishedAt: '2026-03-30T20:35:00Z',
},
],
helper: {
ok: true,
releaseName: 'metapi',
Expand Down Expand Up @@ -142,6 +154,18 @@ describe('updateCenterRuntimeStateService', () => {
displayVersion: 'latest @ sha256:efb2ee655386',
publishedAt: '2026-03-30T20:30:00Z',
},
dockerHubRecentTags: [
{
source: 'docker-hub-tag',
rawVersion: 'dev',
normalizedVersion: 'dev',
url: null,
tagName: 'dev',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
publishedAt: '2026-03-30T20:35:00Z',
},
],
helper: {
ok: true,
releaseName: 'metapi',
Expand Down
9 changes: 9 additions & 0 deletions src/server/services/updateCenterRuntimeStateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { UpdateCenterVersionCandidate, UpdateCenterVersionSource } from './
export type UpdateCenterStatusSnapshot = {
githubRelease: UpdateCenterVersionCandidate | null;
dockerHubTag: UpdateCenterVersionCandidate | null;
dockerHubRecentTags: UpdateCenterVersionCandidate[];
helper: UpdateCenterHelperStatus | null;
};

Expand Down Expand Up @@ -66,6 +67,13 @@ function normalizeVersionCandidate(input: unknown): UpdateCenterVersionCandidate
};
}

function normalizeVersionCandidates(input: unknown): UpdateCenterVersionCandidate[] {
if (!Array.isArray(input)) return [];
return input
.map((entry) => normalizeVersionCandidate(entry))
.filter((entry): entry is UpdateCenterVersionCandidate => !!entry);
}

function normalizeHelperHistoryEntry(input: unknown): NonNullable<UpdateCenterHelperStatus['history']>[number] | null {
if (!input || typeof input !== 'object') return null;
const record = input as Record<string, unknown>;
Expand Down Expand Up @@ -109,6 +117,7 @@ function normalizeStatusSnapshot(input: unknown): UpdateCenterStatusSnapshot | n
return {
githubRelease: normalizeVersionCandidate(record.githubRelease),
dockerHubTag: normalizeVersionCandidate(record.dockerHubTag),
dockerHubRecentTags: normalizeVersionCandidates(record.dockerHubRecentTags),
helper: normalizeHelperSnapshot(record.helper),
};
}
Expand Down
Loading
Loading