feat(distributed): 支持单主控多节点部署和停服迁移
复用现有 k3s 调度和 Job 生命周期,增加 worker 接入与批准、受保护节点身份、归档传输、持久迁移锁及活动 PVC 切换;同步管理员 API、CLI、面板和隔离规则。分布式模式默认关闭,保持单机兼容。 验证:Go 全量测试与 vet;面板 874 个测试、lint/build;Linux VM 安装器测试、清单服务端 dry-run、网络命名空间防火墙实测。A/B/C 三机 WireGuard、Velocity 和迁移验收仍待完成。
This commit is contained in:
77 files changed
+5224
-73
No files matched your search
@@ -0,0 +1,44 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MigrationDialog } from "./DistributedNodes";
|
||||
|
||||
const calls = vi.hoisted(() => ({ nodes: vi.fn(), migration: vi.fn(), retryMigration: vi.fn(), migrateServer: vi.fn() }));
|
||||
vi.mock("@/lib/api", async (importActual) => {
|
||||
const actual = await importActual<typeof import("@/lib/api")>();
|
||||
return { ...actual, api: { ...actual.api, ...calls } };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
calls.nodes.mockResolvedValue([{ name: "c", role: "worker", ready: true, approved: true }]);
|
||||
calls.migration.mockResolvedValue({ id: "persisted-op", state: "failed", sourceNode: "b", targetNode: "c", error: "source node is offline" });
|
||||
calls.retryMigration.mockResolvedValue({ id: "persisted-op", state: "backing_up" });
|
||||
});
|
||||
|
||||
describe("durable migration", () => {
|
||||
it("loads the persisted operation on opening and retries its ID", async () => {
|
||||
const user = userEvent.setup();
|
||||
const changed = vi.fn();
|
||||
render(<MigrationDialog name="survival" nodeName="b" stopped onChanged={changed} />);
|
||||
await user.click(screen.getByRole("button", { name: "Migrate world" }));
|
||||
await screen.findByText("source node is offline");
|
||||
expect(calls.migration).toHaveBeenCalledWith("survival");
|
||||
expect(screen.getByText("persisted-op")).toBeTruthy();
|
||||
await user.click(screen.getByRole("button", { name: "Try again" }));
|
||||
await waitFor(() => expect(calls.retryMigration).toHaveBeenCalledWith("survival", "persisted-op"));
|
||||
expect(changed).toHaveBeenCalled();
|
||||
expect(calls.migrateServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps retry disabled until the server is stopped", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MigrationDialog name="survival" nodeName="b" stopped={false} onChanged={() => {}} />);
|
||||
await user.click(screen.getByRole("button", { name: "Migrate world" }));
|
||||
const retry = await screen.findByRole("button", { name: "Try again" });
|
||||
expect((retry as HTMLButtonElement).disabled).toBe(true);
|
||||
await user.click(retry);
|
||||
expect(calls.retryMigration).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user