Unverified Commit c8957cf1 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(panel): expose distributed deployment in platform settings

parent 9d2de72c
Loading
Loading
Loading
Loading
+2 −0
Changes for docs/distributed.md: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -70,6 +70,8 @@ felis node list

批准前 worker 带 `NoSchedule` 隔离 taint,且没有受保护的 approved 标签。批准命令检查架构、版本、节点在线状态、WireGuard、宿主机隔离、kubelet 修改受保护标签被拒绝,删除指定镜像后执行真实 `crictl pull`。随后创建临时探针,检查跨节点 Service、控制服务的正向可达性和游戏标签 Pod 的拒绝路径。A 从宿主机连接每个测试 Service,读取后端实际观察到的源地址,只将属于 A 的精确地址写入游戏策略。任何检查失败都保留隔离状态。批准过程中会删除指定缓存镜像,须在该节点尚无游戏任务时执行。

Owner 可在面板“平台设置 → 多节点分布式部署”查看部署模式、节点在线及批准状态、接入流程,并进入服务器节点选择与迁移。部署模式由安装器配置;节点批准仍通过主控命令执行完整校验。

节点批准后,管理员可在面板创建服务器时选择它,或在创建请求中传 `nodeName`。普通服主不能选节点或指定 PVC。失联节点拒绝新任务;operator 撤销该服务器 Service 的后端和 Ready 状态,Velocity 进入原有 fallback 流程,不换机。

## 停服迁移
+14 −1
Changes for panel/src/components/DistributedNodes.test.tsx: 14 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
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";
import { DistributedNodes, 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) => {
@@ -42,3 +42,16 @@ describe("durable migration", () => {
    expect(calls.retryMigration).not.toHaveBeenCalled();
  });
});

describe("execution node overview", () => {
  it("shows a failed read and reloads without interpreting failure as an empty cluster", async () => {
    calls.nodes.mockRejectedValueOnce({ status: 503, code: "distributed_unavailable" });
    calls.nodes.mockResolvedValueOnce([]);
    render(<DistributedNodes />);
    expect(await screen.findByRole("alert")).toHaveProperty("textContent", "Distributed deployment is not configured. Configure the controller and worker nodes using the deployment runbook first.");
    expect(screen.queryByText(/No execution nodes were found/)).toBeNull();
    await userEvent.click(screen.getByRole("button", { name: "Reload nodes" }));
    expect(await screen.findByText(/No execution nodes were found/)).toBeTruthy();
    expect(screen.queryByRole("alert")).toBeNull();
  });
});
+14 −7
Changes for panel/src/components/DistributedNodes.tsx: 14 added lines, 7 removed lines.
Original line number Diff line number Diff line
import { RefreshCw, Loader2 } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { api, humanizeError } from "@/lib/api";
@@ -10,21 +13,25 @@ import { Label } from "@/components/ui/label";

export function DistributedNodes() {
  const { t } = useTranslation("servers");
  const nodes = useAsync(api.nodes);
  const nodes = useAsync(api.nodes, [], { coalesce: true });
  usePolling(nodes.reload, 10_000);
  return (
    <div className="rounded-lg border p-4 space-y-2">
      <h2 className="font-medium">{t("nodes")}</h2>
    <Card>
      <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle>{t("nodes")}</CardTitle><Button variant="outline" size="sm" disabled={nodes.loading} onClick={nodes.reload}><RefreshCw />{t("nodes_reload")}</Button></CardHeader>
      <CardContent className="space-y-3">
      {nodes.loading && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("nodes_loading")}</p>}
      {!nodes.loading && !nodes.error && nodes.data?.length === 0 && <p className="text-sm text-muted-foreground">{t("nodes_empty")}</p>}
      {!!nodes.error && <InlineError message={humanizeError(nodes.error)} />}
      <ul className="space-y-1 text-sm">{nodes.data?.map((n) => (
        <li key={n.name} className="flex flex-wrap gap-3">
          <code>{n.name}</code><span>{n.role || t("node_pending")}</span>
          <span>{n.ready ? t("node_online") : t("node_offline")}</span>
          <span>{n.approved ? t("node_approved") : t("node_pending")}</span>
          <code>{n.name}</code><span>{t(n.role === "controller" ? "node_controller" : n.role === "worker" ? "node_worker" : "node_unassigned")}</span>
          <Badge variant={n.ready ? "default" : "destructive"}>{n.ready ? t("node_online") : t("node_offline")}</Badge>
          {n.role === "worker" && <Badge variant={n.approved ? "default" : "muted"}>{n.approved ? t("node_approved") : t("node_pending")}</Badge>}
          <span className="text-muted-foreground">{n.architecture} · {n.addresses.join(", ")}</span>
        </li>
      ))}</ul>
    </div>
      </CardContent>
    </Card>
  );
}

+15 −1
Changes for panel/src/i18n/resources/en-US/admin.json: 15 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -321,5 +321,19 @@
  "platform_unsaved": "Unsaved changes",
  "platform_live": "Saved changes apply immediately on all API replicas.",
  "platform_discard": "Discard changes",
  "platform_save": "Save and apply"
  "platform_save": "Save and apply",
  "platform_distribution_title": "Distributed deployment",
  "platform_distribution_unknown": "Deployment mode unconfirmed",
  "platform_distribution_enabled": "Distributed mode enabled",
  "platform_distribution_disabled": "Single-node mode",
  "platform_distribution_architecture": "One controller runs the API, operator, database, registry, and public entry point. Multiple workers run game servers and maintenance tasks. Multiple controllers, controller high availability, and automatic failover are not supported.",
  "platform_distribution_enable_hint": "Enabling distributed mode requires installer configuration of controller identity, WireGuard, archive transport, and node isolation during a maintenance window. This deployment configuration cannot be switched live from the panel.",
  "platform_distribution_join": "Connect and approve worker nodes",
  "platform_distribution_prepare": "Back up the database and k3s state, configure the controller using the deployment runbook, and update the complete peer address list on all existing nodes. Workers must use the same architecture and k3s version as the controller.",
  "platform_distribution_token": "On the controller, create a separate expiring bootstrap token for each worker as root. Transfer the token file over trusted SSH/SCP. Do not provide the server token or administrator kubeconfig to workers. Replace command placeholders with actual values.",
  "platform_distribution_worker": "Run felis node join on the worker using the runbook, supplying the controller address, fixed node address, token file, Registry address, and complete peer list. New nodes remain isolated with NoSchedule until approval.",
  "platform_distribution_approve": "Approve the node from the controller. The command verifies identity, network isolation, image pulling, and cross-node connectivity. Failed checks preserve isolation. Approval removes the specified cached image and must run before the node hosts game workloads.",
  "platform_distribution_runbook": "Deployment and acceptance runbook",
  "platform_distribution_management": "Online approved workers can be selected when creating a server. Existing servers must be fully stopped before migration. Successful migration leaves the server stopped and retains its source world volume.",
  "platform_distribution_servers": "Manage server placement and migration"
}
+2 −1
Changes for panel/src/i18n/resources/en-US/errors.json: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -144,5 +144,6 @@
  "minecraft_profile_not_found": "The profile was not found in the selected authentication source. Check the profile name or UUID.",
  "auth_sources_unavailable": "Authentication source management is unavailable.",
  "auth_sources_changed": "Authentication sources changed. Discard your edits and reload before saving.",
  "auth_source_tag_locked": "Saved source IDs cannot be renamed or removed. Disable the source instead."
  "auth_source_tag_locked": "Saved source IDs cannot be renamed or removed. Disable the source instead.",
  "distributed_unavailable": "Distributed deployment is not configured. Configure the controller and worker nodes using the deployment runbook first."
}
Loading