fix: migrate code queue control plane to k3s
This commit is contained in:
@@ -80,8 +80,8 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon
|
||||
const frontend = asRecord(item.frontend, `${path}.frontend`);
|
||||
const deployment = item.deployment === undefined ? undefined : asRecord(item.deployment, `${path}.deployment`);
|
||||
const deploymentMode = deployment === undefined ? "unidesk-direct" : stringFromRecord(deployment, "mode", `${path}.deployment`);
|
||||
if (deploymentMode !== "unidesk-direct" && deploymentMode !== "v3sctl-managed") {
|
||||
throw new Error(`${path}.deployment.mode must be unidesk-direct or v3sctl-managed`);
|
||||
if (deploymentMode !== "unidesk-direct" && deploymentMode !== "k3sctl-managed") {
|
||||
throw new Error(`${path}.deployment.mode must be unidesk-direct or k3sctl-managed`);
|
||||
}
|
||||
return {
|
||||
id: stringFromRecord(item, "id", path),
|
||||
@@ -113,7 +113,7 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon
|
||||
: {
|
||||
mode: deploymentMode,
|
||||
adapterServiceId: optionalStringFromRecord(deployment, "adapterServiceId", `${path}.deployment`),
|
||||
v3sServiceId: optionalStringFromRecord(deployment, "v3sServiceId", `${path}.deployment`),
|
||||
k3sServiceId: optionalStringFromRecord(deployment, "k3sServiceId", `${path}.deployment`),
|
||||
namespace: optionalStringFromRecord(deployment, "namespace", `${path}.deployment`),
|
||||
expectedNodeIds: optionalStringArrayFromRecord(deployment, "expectedNodeIds", `${path}.deployment`),
|
||||
activeNodeId: optionalStringFromRecord(deployment, "activeNodeId", `${path}.deployment`),
|
||||
|
||||
@@ -368,8 +368,8 @@ function canDirectProxyMicroservice(service: MicroserviceConfig): boolean {
|
||||
return service.providerId === "main-server";
|
||||
}
|
||||
|
||||
function isV3sctlManagedMicroservice(service: MicroserviceConfig): boolean {
|
||||
return service.deployment.mode === "v3sctl-managed" || service.backend.proxyMode === "v3sctl-adapter-http";
|
||||
function isK3sctlManagedMicroservice(service: MicroserviceConfig): boolean {
|
||||
return service.deployment.mode === "k3sctl-managed" || service.backend.proxyMode === "k3sctl-adapter-http";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -545,7 +545,7 @@ async function directMicroserviceResponse(
|
||||
}
|
||||
}
|
||||
|
||||
async function v3sctlAdapterMicroserviceResponse(
|
||||
async function k3sctlAdapterMicroserviceResponse(
|
||||
service: MicroserviceConfig,
|
||||
method: string,
|
||||
targetPath: string,
|
||||
@@ -554,16 +554,16 @@ async function v3sctlAdapterMicroserviceResponse(
|
||||
bodyText: string,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Response> {
|
||||
const adapterServiceId = service.deployment.adapterServiceId ?? "v3sctl-adapter";
|
||||
const adapterServiceId = service.deployment.adapterServiceId ?? "k3sctl-adapter";
|
||||
const adapter = microserviceById(adapterServiceId);
|
||||
if (adapter === null) {
|
||||
return jsonResponse({ ok: false, error: `v3sctl adapter microservice not found: ${adapterServiceId}`, serviceId: service.id }, 502);
|
||||
return jsonResponse({ ok: false, error: `k3sctl adapter microservice not found: ${adapterServiceId}`, serviceId: service.id }, 502);
|
||||
}
|
||||
if (adapter.id === service.id || isV3sctlManagedMicroservice(adapter)) {
|
||||
return jsonResponse({ ok: false, error: "v3sctl adapter must be a UniDesk-direct microservice", serviceId: service.id, adapterServiceId }, 500);
|
||||
if (adapter.id === service.id || isK3sctlManagedMicroservice(adapter)) {
|
||||
return jsonResponse({ ok: false, error: "k3sctl adapter must be a UniDesk-direct microservice", serviceId: service.id, adapterServiceId }, 500);
|
||||
}
|
||||
const v3sServiceId = service.deployment.v3sServiceId ?? service.id;
|
||||
const adapterTargetPath = `/api/services/${encodeURIComponent(v3sServiceId)}/proxy${targetPath}`;
|
||||
const k3sServiceId = service.deployment.k3sServiceId ?? service.id;
|
||||
const adapterTargetPath = `/api/services/${encodeURIComponent(k3sServiceId)}/proxy${targetPath}`;
|
||||
return fetchMicroserviceUpstreamResponse(adapter, method, adapterTargetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
||||
}
|
||||
|
||||
@@ -576,8 +576,8 @@ async function fetchMicroserviceUpstreamResponse(
|
||||
bodyText: string,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Response> {
|
||||
if (isV3sctlManagedMicroservice(service)) {
|
||||
return v3sctlAdapterMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
||||
if (isK3sctlManagedMicroservice(service)) {
|
||||
return k3sctlAdapterMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
||||
}
|
||||
if (canDirectProxyMicroservice(service)) {
|
||||
return directMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
||||
@@ -716,12 +716,12 @@ export async function getMicroservices(): Promise<JsonValue[]> {
|
||||
return config().microservices.map((service) => {
|
||||
const node = nodes.find((item) => item.providerId === service.providerId) ?? null;
|
||||
const docker = dockerStatuses.find((item) => item.providerId === service.providerId) ?? null;
|
||||
const v3sManaged = isV3sctlManagedMicroservice(service);
|
||||
const container = v3sManaged ? null : findContainer(docker?.dockerStatus ?? null, service.repository.containerName);
|
||||
const k3sManaged = isK3sctlManagedMicroservice(service);
|
||||
const container = k3sManaged ? null : findContainer(docker?.dockerStatus ?? null, service.repository.containerName);
|
||||
return {
|
||||
...service,
|
||||
runtime: {
|
||||
orchestrator: v3sManaged ? "v3sctl" : "unidesk-direct",
|
||||
orchestrator: k3sManaged ? "k3sctl" : "unidesk-direct",
|
||||
providerStatus: node?.status ?? "missing",
|
||||
providerName: node?.name ?? service.providerId,
|
||||
providerLastHeartbeat: node?.lastHeartbeat ?? null,
|
||||
|
||||
@@ -45,9 +45,9 @@ export interface MicroserviceConfig {
|
||||
timeoutMs: number;
|
||||
};
|
||||
deployment: {
|
||||
mode: "unidesk-direct" | "v3sctl-managed";
|
||||
mode: "unidesk-direct" | "k3sctl-managed";
|
||||
adapterServiceId?: string;
|
||||
v3sServiceId?: string;
|
||||
k3sServiceId?: string;
|
||||
namespace?: string;
|
||||
expectedNodeIds?: string[];
|
||||
activeNodeId?: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6291,11 +6291,11 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.v3s-page {
|
||||
.k3s-page {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
.v3s-hero-panel {
|
||||
.k3s-hero-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: rgba(215, 161, 58, 0.34);
|
||||
@@ -6304,14 +6304,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
radial-gradient(circle at 12% 84%, rgba(78, 183, 168, 0.13), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255,255,255,0.045), rgba(255,255,255,0.012));
|
||||
}
|
||||
.v3s-hero {
|
||||
.k3s-hero {
|
||||
display: grid;
|
||||
grid-template-columns: 128px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.v3s-orb {
|
||||
.k3s-orb {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -6328,20 +6328,20 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
color: var(--accent);
|
||||
font-weight: 800;
|
||||
}
|
||||
.v3s-orb::before,
|
||||
.v3s-orb::after {
|
||||
.k3s-orb::before,
|
||||
.k3s-orb::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 12px;
|
||||
border: 1px dashed rgba(78,183,168,0.38);
|
||||
border-radius: inherit;
|
||||
}
|
||||
.v3s-orb::after {
|
||||
.k3s-orb::after {
|
||||
inset: 28px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255,255,255,0.12);
|
||||
}
|
||||
.v3s-hero-copy h2 {
|
||||
.k3s-hero-copy h2 {
|
||||
max-width: 860px;
|
||||
margin: 0 0 8px;
|
||||
color: var(--text);
|
||||
@@ -6350,14 +6350,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: none;
|
||||
}
|
||||
.v3s-route-strip {
|
||||
.k3s-route-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.v3s-route-strip span {
|
||||
.k3s-route-strip span {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid rgba(215,161,58,0.5);
|
||||
border-radius: 999px;
|
||||
@@ -6367,7 +6367,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
.v3s-route-strip code {
|
||||
.k3s-route-strip code {
|
||||
max-width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--line-soft);
|
||||
@@ -6377,79 +6377,79 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
.v3s-control-plane-grid {
|
||||
.k3s-control-plane-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.v3s-control-plane-card {
|
||||
.k3s-control-plane-card {
|
||||
min-height: 96px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.22), rgba(78,183,168,0.07));
|
||||
}
|
||||
.v3s-control-plane-card span {
|
||||
.k3s-control-plane-card span {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.v3s-control-plane-card strong {
|
||||
.k3s-control-plane-card strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--text);
|
||||
font-size: 17px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.v3s-control-plane-card p {
|
||||
.k3s-control-plane-card p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.v3s-service-panel {
|
||||
.k3s-service-panel {
|
||||
border-color: rgba(78,183,168,0.25);
|
||||
}
|
||||
.v3s-service-summary {
|
||||
.k3s-service-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.v3s-service-summary > div,
|
||||
.v3s-instance-card {
|
||||
.k3s-service-summary > div,
|
||||
.k3s-instance-card {
|
||||
border: 1px solid var(--line-soft);
|
||||
background: rgba(0,0,0,0.18);
|
||||
}
|
||||
.v3s-service-summary > div {
|
||||
.k3s-service-summary > div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 66px;
|
||||
padding: 10px;
|
||||
}
|
||||
.v3s-service-summary span,
|
||||
.v3s-kv dt {
|
||||
.k3s-service-summary span,
|
||||
.k3s-kv dt {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.v3s-service-summary strong {
|
||||
.k3s-service-summary strong {
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
}
|
||||
.v3s-instance-grid {
|
||||
.k3s-instance-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.v3s-instance-card {
|
||||
.k3s-instance-card {
|
||||
padding: 12px;
|
||||
border-left: 3px solid rgba(78,183,168,0.7);
|
||||
}
|
||||
.v3s-instance-role {
|
||||
.k3s-instance-role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -6459,34 +6459,34 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
.v3s-kv {
|
||||
.k3s-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
gap: 6px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.v3s-kv dd {
|
||||
.k3s-kv dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.v3s-kv code {
|
||||
.k3s-kv code {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.v3s-hero {
|
||||
.k3s-hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.v3s-orb {
|
||||
.k3s-orb {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.v3s-service-summary {
|
||||
.k3s-service-summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.v3s-control-plane-grid {
|
||||
.k3s-control-plane-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PipelinePage } from "./pipeline";
|
||||
import { ProjectManagerPage } from "./project-manager";
|
||||
import { TodoNotePage } from "./todo-note";
|
||||
import { TopStatusBar } from "./top-status";
|
||||
import { V3sCtlPage } from "./v3sctl";
|
||||
import { K3sCtlPage } from "./k3sctl";
|
||||
import { LoadingTitle } from "./loading-indicator";
|
||||
import { errorMessage, requestJson } from "./unidesk-error";
|
||||
import { UniDeskErrorBanner } from "./unidesk-error-banner";
|
||||
@@ -46,21 +46,21 @@ const fastCodeQueueService = {
|
||||
name: "Code Queue",
|
||||
providerId: "D601",
|
||||
description: "Code Queue",
|
||||
repository: { containerName: "v3s:code-queue" },
|
||||
repository: { containerName: "k3s:code-queue" },
|
||||
backend: {
|
||||
nodeBaseUrl: "v3s://code-queue",
|
||||
nodeBindHost: "v3s://unidesk/code-queue",
|
||||
nodeBaseUrl: "k3s://code-queue",
|
||||
nodeBindHost: "k3s://unidesk/code-queue",
|
||||
nodePort: 4222,
|
||||
proxyMode: "v3sctl-adapter-http",
|
||||
proxyMode: "k3sctl-adapter-http",
|
||||
public: false,
|
||||
},
|
||||
deployment: {
|
||||
mode: "v3sctl-managed",
|
||||
adapterServiceId: "v3sctl-adapter",
|
||||
v3sServiceId: "code-queue",
|
||||
mode: "k3sctl-managed",
|
||||
adapterServiceId: "k3sctl-adapter",
|
||||
k3sServiceId: "code-queue",
|
||||
},
|
||||
runtime: {
|
||||
orchestrator: "v3sctl",
|
||||
orchestrator: "k3sctl",
|
||||
providerStatus: "loading",
|
||||
providerName: "D601",
|
||||
},
|
||||
@@ -1649,7 +1649,7 @@ function MicroserviceCatalogPage({ microservices, onRaw, onNavigate }: AnyRecord
|
||||
service.id === "claudeqq" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "claudeqq"), "data-testid": "open-claudeqq-button" }, "打开") : null,
|
||||
service.id === "baidu-netdisk" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "baidu-netdisk"), "data-testid": "open-baidu-netdisk-button" }, "打开") : null,
|
||||
service.id === "oa-event-flow" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "oa-event-flow"), "data-testid": "open-oa-event-flow-button" }, "打开") : null,
|
||||
service.id === "v3sctl-adapter" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "v3sctl"), "data-testid": "open-v3sctl-button" }, "打开") : null,
|
||||
service.id === "k3sctl-adapter" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "k3sctl"), "data-testid": "open-k3sctl-button" }, "打开") : null,
|
||||
service.id === "code-queue" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "open-code-queue-button" }, "打开") : null,
|
||||
service.id === "mdtodo" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "mdtodo"), "data-testid": "open-mdtodo-button" }, "打开") : null,
|
||||
service.id === "project-manager" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "project-manager"), "data-testid": "open-project-manager-button" }, "打开") : null,
|
||||
@@ -2157,7 +2157,7 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa
|
||||
if (activeModule === "apps" && activeTab === "baidu-netdisk") return h(BaiduNetdiskPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "filebrowser") return h(FileBrowserPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "oa-event-flow") return h(OaEventFlowPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "v3sctl") return h(V3sCtlPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, onNavigate });
|
||||
if (activeModule === "apps" && activeTab === "k3sctl") return h(K3sCtlPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, onNavigate });
|
||||
if (activeModule === "apps" && activeTab === "code-queue") return h(CodeQueuePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, initialTasksData: initialCodeQueueOverview });
|
||||
if (activeModule === "apps" && activeTab === "mdtodo") return h(MdtodoPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "project-manager") return h(ProjectManagerPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
@@ -2188,7 +2188,7 @@ function Shell({ session, onLogout }: AnyRecord) {
|
||||
: microservices;
|
||||
const effectiveData = effectiveMicroservices === microservices ? data : { ...data, microservices: effectiveMicroservices };
|
||||
const activeService = activeModule === "apps"
|
||||
? effectiveMicroservices.find((service: any) => String(service?.id || "") === (activeTab === "v3sctl" ? "v3sctl-adapter" : activeTab))
|
||||
? effectiveMicroservices.find((service: any) => String(service?.id || "") === (activeTab === "k3sctl" ? "k3sctl-adapter" : activeTab))
|
||||
: null;
|
||||
const activeServiceRuntime = activeService ? microserviceRuntime(activeService) : {};
|
||||
const activeTabTitle = module.tabs.find((tab: any) => tab.id === activeTab)?.label || activeTab;
|
||||
|
||||
@@ -30,22 +30,22 @@ const standaloneCodeQueueService = {
|
||||
id: "code-queue",
|
||||
name: "Code Queue",
|
||||
providerId: "D601",
|
||||
description: "Code Queue 独立入口,使用 v3sctl-adapter 单一路径访问 active service。",
|
||||
repository: { containerName: "v3s:code-queue" },
|
||||
description: "Code Queue 独立入口,使用 k3sctl-adapter 单一路径访问 active service。",
|
||||
repository: { containerName: "k3s:code-queue" },
|
||||
backend: {
|
||||
nodeBaseUrl: "v3s://code-queue",
|
||||
nodeBindHost: "v3s://unidesk/code-queue",
|
||||
nodeBaseUrl: "k3s://code-queue",
|
||||
nodeBindHost: "k3s://unidesk/code-queue",
|
||||
nodePort: 4222,
|
||||
proxyMode: "v3sctl-adapter-http",
|
||||
proxyMode: "k3sctl-adapter-http",
|
||||
public: false,
|
||||
},
|
||||
deployment: {
|
||||
mode: "v3sctl-managed",
|
||||
adapterServiceId: "v3sctl-adapter",
|
||||
v3sServiceId: "code-queue",
|
||||
mode: "k3sctl-managed",
|
||||
adapterServiceId: "k3sctl-adapter",
|
||||
k3sServiceId: "code-queue",
|
||||
},
|
||||
runtime: {
|
||||
orchestrator: "v3sctl",
|
||||
orchestrator: "k3sctl",
|
||||
providerStatus: "online",
|
||||
providerName: "D601",
|
||||
},
|
||||
|
||||
@@ -57,11 +57,11 @@ function microserviceRuntime(service: any): AnyRecord {
|
||||
}
|
||||
|
||||
function proxy(apiBaseUrl: string, path: string): string {
|
||||
return `${apiBaseUrl}/microservices/v3sctl-adapter/proxy${path}`;
|
||||
return `${apiBaseUrl}/microservices/k3sctl-adapter/proxy${path}`;
|
||||
}
|
||||
|
||||
function findAdapter(microservices: any[]): any | null {
|
||||
return microservices.find((service) => String(service?.id || "") === "v3sctl-adapter") || null;
|
||||
return microservices.find((service) => String(service?.id || "") === "k3sctl-adapter") || null;
|
||||
}
|
||||
|
||||
function instanceTone(instance: any): string {
|
||||
@@ -90,16 +90,16 @@ function primaryInstance(services: any[]): string {
|
||||
}
|
||||
|
||||
function renderInstanceCard(instance: any) {
|
||||
return h("article", { key: instance?.id || instance?.nodeId, className: "v3s-instance-card" },
|
||||
return h("article", { key: instance?.id || instance?.nodeId, className: "k3s-instance-card" },
|
||||
h("div", { className: "node-card-head" },
|
||||
h("strong", null, instance?.nodeId || instance?.id || "--"),
|
||||
h(StatusBadge, { status: instanceTone(instance) }, instance?.healthy ? "HEALTHY" : "DEGRADED"),
|
||||
),
|
||||
h("div", { className: "v3s-instance-role" },
|
||||
h("div", { className: "k3s-instance-role" },
|
||||
h("span", null, String(instance?.role || "worker").toUpperCase()),
|
||||
h("code", null, instance?.id || "--"),
|
||||
),
|
||||
h("dl", { className: "v3s-kv" },
|
||||
h("dl", { className: "k3s-kv" },
|
||||
h("dt", null, "Base URL"), h("dd", null, h("code", null, instance?.baseUrl || "--")),
|
||||
h("dt", null, "Proxy"), h("dd", null, instance?.proxyMode || "--"),
|
||||
h("dt", null, "Health"), h("dd", null, `${instance?.upstreamStatus ?? "--"} / ${instance?.status || "unknown"}`),
|
||||
@@ -114,23 +114,23 @@ function renderManagedService(service: any, onRaw: any) {
|
||||
return h(Panel, {
|
||||
key: service?.id || "service",
|
||||
title: service?.id || "managed-service",
|
||||
eyebrow: `${service?.namespace || "unidesk"} / v3s managed service`,
|
||||
className: "v3s-service-panel",
|
||||
actions: h(RawButton, { title: `v3s service ${service?.id || ""}`, data: service, onOpen: onRaw, testId: `raw-v3s-service-${service?.id || "unknown"}` }),
|
||||
eyebrow: `${service?.namespace || "unidesk"} / k3s managed service`,
|
||||
className: "k3s-service-panel",
|
||||
actions: h(RawButton, { title: `k3s service ${service?.id || ""}`, data: service, onOpen: onRaw, testId: `raw-k3s-service-${service?.id || "unknown"}` }),
|
||||
},
|
||||
h("div", { className: "v3s-service-summary" },
|
||||
h("div", { className: "k3s-service-summary" },
|
||||
h("div", null, h("span", null, "状态"), h(StatusBadge, { status: serviceTone(service) }, service?.status || "unknown")),
|
||||
h("div", null, h("span", null, "Active"), h("strong", null, service?.activeInstanceId || "--")),
|
||||
h("div", null, h("span", null, "Single Writer"), h("strong", null, fmtBool(service?.singleWriter))),
|
||||
h("div", null, h("span", null, "Active Health"), h("strong", null, active?.upstreamStatus ?? "--")),
|
||||
),
|
||||
instances.length === 0
|
||||
? h(EmptyState, { title: "暂无 v3s 实例", text: "adapter 没有返回该服务的 endpoint 列表" })
|
||||
: h("div", { className: "v3s-instance-grid" }, instances.map(renderInstanceCard)),
|
||||
? h(EmptyState, { title: "暂无 k3s 实例", text: "adapter 没有返回该服务的 endpoint 列表" })
|
||||
: h("div", { className: "k3s-instance-grid" }, instances.map(renderInstanceCard)),
|
||||
);
|
||||
}
|
||||
|
||||
export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: AnyRecord) {
|
||||
export function K3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: AnyRecord) {
|
||||
const adapter = findAdapter(Array.isArray(microservices) ? microservices : []);
|
||||
const runtime = microserviceRuntime(adapter);
|
||||
const [state, setState] = useState({ loading: false, error: "", data: null as any, refreshedAt: null as Date | null });
|
||||
@@ -141,7 +141,7 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any
|
||||
const data = await requestJson(proxy(apiBaseUrl, "/api/control-plane"));
|
||||
setState({ loading: false, error: "", data, refreshedAt: new Date() });
|
||||
} catch (err) {
|
||||
setState((previous: any) => ({ ...previous, loading: false, error: errorMessage(err, "加载 v3s 控制平面失败") }));
|
||||
setState((previous: any) => ({ ...previous, loading: false, error: errorMessage(err, "加载 k3s 控制平面失败") }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,50 +157,50 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any
|
||||
const kubeProxy = objectRecord(state.data?.kubeApiProxy);
|
||||
const manifestPaths = asArray(state.data?.manifestPaths).map((item) => String(item));
|
||||
|
||||
if (!adapter) return h(EmptyState, { title: "v3sctl-adapter 未登记", text: "请在 config.json 的 microservices 中登记 id=v3sctl-adapter,并通过该微服务连接 v3s 控制平面。" });
|
||||
if (!adapter) return h(EmptyState, { title: "k3sctl-adapter 未登记", text: "请在 config.json 的 microservices 中登记 id=k3sctl-adapter,并通过该微服务连接 k3s 控制平面。" });
|
||||
|
||||
return h("div", { className: "v3s-page", "data-testid": "v3sctl-page" },
|
||||
return h("div", { className: "k3s-page", "data-testid": "k3sctl-page" },
|
||||
h(Panel, {
|
||||
title: "V3S Control Plane",
|
||||
eyebrow: "Managed by v3sctl-adapter",
|
||||
className: "v3s-hero-panel",
|
||||
title: "K3S Control Plane",
|
||||
eyebrow: "Managed by k3sctl-adapter",
|
||||
className: "k3s-hero-panel",
|
||||
loading: state.loading,
|
||||
actions: h(React.Fragment, null,
|
||||
h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "v3s-refresh-button" }, state.loading ? "刷新中" : "刷新"),
|
||||
onNavigate ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "v3s-open-code-queue" }, "打开 Code Queue") : null,
|
||||
h(RawButton, { title: "v3sctl-adapter microservice", data: adapter, onOpen: onRaw, testId: "raw-v3s-adapter" }),
|
||||
h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "k3s-refresh-button" }, state.loading ? "刷新中" : "刷新"),
|
||||
onNavigate ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "k3s-open-code-queue" }, "打开 Code Queue") : null,
|
||||
h(RawButton, { title: "k3sctl-adapter microservice", data: adapter, onOpen: onRaw, testId: "raw-k3s-adapter" }),
|
||||
),
|
||||
},
|
||||
h("div", { className: "v3s-hero" },
|
||||
h("div", { className: "v3s-orb", "aria-hidden": "true" }, h("span", null, "v3s")),
|
||||
h("div", { className: "v3s-hero-copy" },
|
||||
h("div", { className: "k3s-hero" },
|
||||
h("div", { className: "k3s-orb", "aria-hidden": "true" }, h("span", null, "k3s")),
|
||||
h("div", { className: "k3s-hero-copy" },
|
||||
h("p", { className: "eyebrow" }, "D601 control plane / D518 managed node"),
|
||||
h("h2", null, "UniDesk 只管理 adapter;业务微服务交给 v3s 标准服务路由"),
|
||||
h("p", { className: "muted paragraph" }, "Code Queue 的前端/API 请求进入 v3sctl-adapter,再由 adapter 转发到 v3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"),
|
||||
h("div", { className: "v3s-route-strip" },
|
||||
h("h2", null, "UniDesk 只管理 adapter;业务微服务交给 k3s 标准服务路由"),
|
||||
h("p", { className: "muted paragraph" }, "Code Queue 的前端/API 请求进入 k3sctl-adapter,再由 adapter 转发到 k3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"),
|
||||
h("div", { className: "k3s-route-strip" },
|
||||
h("span", null, "NO FALLBACK"),
|
||||
h("code", null, state.data?.runtimePath || "frontend -> backend-core -> v3sctl-adapter"),
|
||||
h("code", null, state.data?.runtimePath || "frontend -> backend-core -> k3sctl-adapter"),
|
||||
),
|
||||
),
|
||||
),
|
||||
h("div", { className: "metric-grid" },
|
||||
h(MetricCard, { label: "控制面", value: state.data?.clusterId || "D601", hint: `adapter ${runtime.providerStatus || "unknown"}`, tone: runtime.providerStatus === "online" ? "ok" : "warn" }),
|
||||
h(MetricCard, { label: "代管服务", value: services.length, hint: `${healthyServices}/${services.length || 0} healthy`, tone: healthyServices === services.length && services.length > 0 ? "ok" : "warn" }),
|
||||
h(MetricCard, { label: "节点", value: nodes.join(" / ") || "--", hint: "expected v3s nodes" }),
|
||||
h(MetricCard, { label: "节点", value: nodes.join(" / ") || "--", hint: "expected k3s nodes" }),
|
||||
h(MetricCard, { label: "实例", value: `${healthyInstances}/${totalInstances}`, hint: `active ${primary}`, tone: healthyInstances === totalInstances && totalInstances > 0 ? "ok" : "warn" }),
|
||||
),
|
||||
h("div", { className: "v3s-control-plane-grid" },
|
||||
h("article", { className: "v3s-control-plane-card" },
|
||||
h("div", { className: "k3s-control-plane-grid" },
|
||||
h("article", { className: "k3s-control-plane-card" },
|
||||
h("span", null, "service proxy"),
|
||||
h("strong", null, kubeProxy.configured === true ? "K8S API PROXY" : "PROXY DEGRADED"),
|
||||
h("p", null, kubeProxy.configured === true ? `${kubeProxy.mode || "kubernetes-api-service-proxy"} via ${kubeProxy.connectHost || "--"}` : "adapter 必须通过 k8s API service proxy 访问业务服务,不回退到业务容器直连。"),
|
||||
),
|
||||
h("article", { className: "v3s-control-plane-card" },
|
||||
h("article", { className: "k3s-control-plane-card" },
|
||||
h("span", null, "manifests"),
|
||||
h("strong", null, manifestPaths.length || "--"),
|
||||
h("p", null, manifestPaths.join(" / ") || "未配置 manifest"),
|
||||
),
|
||||
h("article", { className: "v3s-control-plane-card" },
|
||||
h("article", { className: "k3s-control-plane-card" },
|
||||
h("span", null, "cluster snapshot"),
|
||||
h("strong", null, kubectl.enabled === true ? (kubectl.ok === true ? "KUBECTL OK" : "KUBECTL DEGRADED") : "API ONLY"),
|
||||
h("p", null, kubectl.enabled === true ? `nodes ${kubectl.nodeCount ?? "--"}` : "控制面页面以 adapter 返回的 k8s service proxy 状态为准;kubectl 只作为可选快照。"),
|
||||
@@ -210,7 +210,7 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any
|
||||
state.refreshedAt ? h("p", { className: "muted paragraph" }, `最近刷新 ${fmtClock(state.refreshedAt)}`) : null,
|
||||
),
|
||||
services.length === 0
|
||||
? h(Panel, { title: "代管服务", eyebrow: "v3s services", loading: state.loading }, h(EmptyState, { title: "暂无 v3s 服务", text: "等待 v3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。" }))
|
||||
? h(Panel, { title: "代管服务", eyebrow: "k3s services", loading: state.loading }, h(EmptyState, { title: "暂无 k3s 服务", text: "等待 k3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。" }))
|
||||
: services.map((service) => renderManagedService(service, onRaw)),
|
||||
);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export const MODULES: UniDeskModuleDefinition[] = [
|
||||
{ id: "baidu-netdisk", label: "Baidu Netdisk" },
|
||||
{ id: "filebrowser", label: "File Browser" },
|
||||
{ id: "oa-event-flow", label: "OA Event Flow" },
|
||||
{ id: "v3sctl", label: "V3S Control" },
|
||||
{ id: "k3sctl", label: "K3S Control" },
|
||||
{ id: "code-queue", label: "Code Queue" },
|
||||
{ id: "mdtodo", label: "MDTODO" },
|
||||
{ id: "project-manager", label: "Project Manager" },
|
||||
|
||||
@@ -3109,7 +3109,7 @@ function queuedStatusReason(task: QueueTask, tasks: QueueTask[] = state.tasks):
|
||||
return queuedReason("service", "SERVICE", "Code Queue is still starting and has not enabled scheduling yet.");
|
||||
}
|
||||
if (!config.schedulerEnabled) {
|
||||
return queuedReason("scheduler_disabled", "STANDBY", "This Code Queue instance is a v3s-managed standby and does not start queued work.");
|
||||
return queuedReason("scheduler_disabled", "STANDBY", "This Code Queue instance is a k3s-managed standby and does not start queued work.");
|
||||
}
|
||||
if (mergingQueues.has(queueId)) {
|
||||
return queuedReason("queue_merge", "MERGING", "Queue merge is rewriting queue membership; scheduling will resume immediately after it finishes.");
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
ARG V3SCTL_ADAPTER_BASE_IMAGE=oven/bun:1-debian
|
||||
FROM ${V3SCTL_ADAPTER_BASE_IMAGE}
|
||||
ARG K3SCTL_ADAPTER_BASE_IMAGE=oven/bun:1-debian
|
||||
FROM ${K3SCTL_ADAPTER_BASE_IMAGE}
|
||||
|
||||
# Never build the adapter FROM a service image: inherited Docker Desktop labels
|
||||
# can silently republish old Code Queue ports and mounts.
|
||||
@@ -12,12 +12,12 @@ RUN (command -v curl >/dev/null 2>&1 && command -v ssh >/dev/null 2>&1 && comman
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*)
|
||||
|
||||
WORKDIR /app/src/components/microservices/v3sctl-adapter
|
||||
WORKDIR /app/src/components/microservices/k3sctl-adapter
|
||||
COPY src/components/shared /app/src/components/shared
|
||||
COPY src/components/microservices/v3sctl-adapter/package.json ./package.json
|
||||
COPY src/components/microservices/v3sctl-adapter/tsconfig.json ./tsconfig.json
|
||||
COPY src/components/microservices/v3sctl-adapter/src ./src
|
||||
COPY src/components/microservices/v3sctl-adapter/v3s ./v3s
|
||||
COPY src/components/microservices/k3sctl-adapter/package.json ./package.json
|
||||
COPY src/components/microservices/k3sctl-adapter/tsconfig.json ./tsconfig.json
|
||||
COPY src/components/microservices/k3sctl-adapter/src ./src
|
||||
COPY src/components/microservices/k3sctl-adapter/k3s ./k3s
|
||||
|
||||
EXPOSE 4266
|
||||
CMD ["bun", "--smol", "run", "src/index.ts"]
|
||||
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
k3sctl-adapter:
|
||||
image: unidesk-k3sctl-adapter:d601
|
||||
build:
|
||||
context: ../../../..
|
||||
dockerfile: src/components/microservices/k3sctl-adapter/Dockerfile
|
||||
args:
|
||||
K3SCTL_ADAPTER_BASE_IMAGE: ${K3SCTL_ADAPTER_BASE_IMAGE:-oven/bun:1-debian}
|
||||
container_name: k3sctl-adapter
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${K3SCTL_ADAPTER_ENV_FILE:-../../../../.state/k3sctl-adapter-d601.env}
|
||||
required: false
|
||||
ports:
|
||||
- "127.0.0.1:${K3SCTL_ADAPTER_HOST_PORT:-4266}:4266"
|
||||
environment:
|
||||
HOST: "0.0.0.0"
|
||||
PORT: "4266"
|
||||
LOG_FILE: "/var/log/unidesk/k3sctl-adapter.jsonl"
|
||||
K3SCTL_CLUSTER_ID: "${K3SCTL_CLUSTER_ID:-unidesk-k3s}"
|
||||
K3SCTL_NODE_ID: "${K3SCTL_NODE_ID:-D601}"
|
||||
K3SCTL_KUBECTL_ENABLED: "${K3SCTL_KUBECTL_ENABLED:-false}"
|
||||
K3SCTL_KUBE_API_PROXY_ENABLED: "${K3SCTL_KUBE_API_PROXY_ENABLED:-true}"
|
||||
K3SCTL_KUBECONFIG_PATH: "/var/lib/unidesk/k3s/kubeconfig"
|
||||
K3SCTL_KUBE_API_CONNECT_HOST: "${K3SCTL_KUBE_API_CONNECT_HOST:-host.docker.internal}"
|
||||
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json}"
|
||||
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
|
||||
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
|
||||
volumes:
|
||||
- ${K3SCTL_ADAPTER_LOG_DIR:-../../../../.state/k3sctl-adapter/logs}:/var/log/unidesk
|
||||
- ${K3SCTL_KUBECONFIG_HOST_PATH:-/etc/rancher/k3s/k3s.yaml}:/var/lib/unidesk/k3s/kubeconfig:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- default
|
||||
- provider-gateway
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS --max-time 2 http://127.0.0.1:4266/health >/dev/null"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
networks:
|
||||
provider-gateway:
|
||||
external: true
|
||||
name: ${K3SCTL_PROVIDER_GATEWAY_NETWORK:-unidesk-provider-d601_default}
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"apiVersion": "unidesk.ai/v3s/v1",
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "v3sctl-adapter",
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-v8s",
|
||||
"context": "unidesk-v8s"
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
+7
-7
@@ -4,7 +4,7 @@ metadata:
|
||||
name: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/v3s-cluster: unidesk-v8s
|
||||
unidesk.ai/k3s-cluster: unidesk-k3s
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -255,7 +255,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
spec:
|
||||
replicas: 1
|
||||
@@ -268,7 +268,7 @@ spec:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
unidesk.ai/node-id: D601
|
||||
spec:
|
||||
@@ -480,7 +480,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
@@ -500,7 +500,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D518
|
||||
spec:
|
||||
replicas: 1
|
||||
@@ -513,7 +513,7 @@ spec:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D518
|
||||
unidesk.ai/node-id: D518
|
||||
spec:
|
||||
@@ -725,7 +725,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D518
|
||||
spec:
|
||||
type: ClusterIP
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"apiVersion": "unidesk.ai/v3s/v1",
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "mdtodo",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "v3sctl-adapter",
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-v8s",
|
||||
"context": "unidesk-v8s"
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
+3
-3
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: mdtodo
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
spec:
|
||||
replicas: 1
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
labels:
|
||||
app.kubernetes.io/name: mdtodo
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
unidesk.ai/node-id: D601
|
||||
spec:
|
||||
@@ -94,7 +94,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: mdtodo
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: v3sctl-managed
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@unidesk/v3sctl-adapter",
|
||||
"name": "@unidesk/k3sctl-adapter",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
+31
-31
@@ -64,8 +64,8 @@ const config = readConfig();
|
||||
const logWriter = config.logFile
|
||||
? createHourlyJsonlWriter({
|
||||
baseLogFile: config.logFile,
|
||||
service: "v3sctl-adapter",
|
||||
maxBytes: logRetentionBytesForService("v3sctl-adapter"),
|
||||
service: "k3sctl-adapter",
|
||||
maxBytes: logRetentionBytesForService("k3sctl-adapter"),
|
||||
})
|
||||
: null;
|
||||
const kubeClient = loadKubeApiClient();
|
||||
@@ -172,7 +172,7 @@ function parseManagedKubernetesManifest(value: unknown, index: number, ownerPath
|
||||
const spec = asRecord(manifest.spec, `${path}.spec`);
|
||||
const kind = optionalStringField(manifest, "kind", "ManagedKubernetesService");
|
||||
const serviceId = stringField(metadata, "name", `${path}.metadata`);
|
||||
if (kind !== "ManagedKubernetesService") throw new Error(`${path}.kind must be ManagedKubernetesService; direct ManagedHttpService manifests are not allowed in pure v3s mode`);
|
||||
if (kind !== "ManagedKubernetesService") throw new Error(`${path}.kind must be ManagedKubernetesService; direct ManagedHttpService manifests are not allowed in pure k3s mode`);
|
||||
const instancesRaw = spec.instances;
|
||||
if (!Array.isArray(instancesRaw) || instancesRaw.length === 0) throw new Error(`${path}.spec.instances must be a non-empty array`);
|
||||
const endpoints = instancesRaw.map((endpoint, endpointIndex) => parseEndpoint(endpoint, endpointIndex, `${path}.spec.instances`));
|
||||
@@ -200,12 +200,12 @@ function parseServiceOrManifest(value: unknown, index: number, ownerPath = "serv
|
||||
if (typeof item.kind === "string" || item.metadata !== undefined || item.spec !== undefined) {
|
||||
return parseManagedKubernetesManifest(item, index, ownerPath);
|
||||
}
|
||||
throw new Error(`${ownerPath}[${index}] must be a ManagedKubernetesService manifest; static HTTP service declarations are not allowed in pure v3s mode`);
|
||||
throw new Error(`${ownerPath}[${index}] must be a ManagedKubernetesService manifest; static HTTP service declarations are not allowed in pure k3s mode`);
|
||||
}
|
||||
|
||||
function parseServices(raw: string, ownerPath = "V3SCTL_SERVICES_JSON"): ManagedService[] {
|
||||
function parseServices(raw: string, ownerPath = "K3SCTL_SERVICES_JSON"): ManagedService[] {
|
||||
const value = raw.trim().length === 0 ? [] : JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(value)) throw new Error("V3SCTL_SERVICES_JSON must be an array");
|
||||
if (!Array.isArray(value)) throw new Error("K3SCTL_SERVICES_JSON must be an array");
|
||||
return value.map((item, index) => parseServiceOrManifest(item, index, ownerPath));
|
||||
}
|
||||
|
||||
@@ -237,36 +237,36 @@ function mergeServices(services: ManagedService[]): ManagedService[] {
|
||||
const seen = new Set<string>();
|
||||
for (const service of services) {
|
||||
const key = `${service.namespace}/${service.id}`;
|
||||
if (seen.has(key)) throw new Error(`duplicate v3s managed service: ${key}`);
|
||||
if (seen.has(key)) throw new Error(`duplicate k3s managed service: ${key}`);
|
||||
seen.add(key);
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
function readConfig(): RuntimeConfig {
|
||||
const paths = manifestPaths(envString("V3SCTL_MANIFEST_PATHS", "v3s/code-queue.v3s.json"));
|
||||
const inlineServices = parseServices(envString("V3SCTL_SERVICES_JSON", "[]"));
|
||||
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json"));
|
||||
const inlineServices = parseServices(envString("K3SCTL_SERVICES_JSON", "[]"));
|
||||
const manifestServices = readManifestServices(paths);
|
||||
return {
|
||||
host: envString("HOST", "0.0.0.0"),
|
||||
port: envNumber("PORT", 4266),
|
||||
logFile: envString("LOG_FILE", "/var/log/unidesk/v3sctl-adapter.jsonl"),
|
||||
logFile: envString("LOG_FILE", "/var/log/unidesk/k3sctl-adapter.jsonl"),
|
||||
manifestPaths: paths,
|
||||
clusterId: envString("V3SCTL_CLUSTER_ID", "unidesk-v8s"),
|
||||
nodeId: envString("V3SCTL_NODE_ID", "D601"),
|
||||
kubectlEnabled: envBool("V3SCTL_KUBECTL_ENABLED", false),
|
||||
kubectlContext: envString("V3SCTL_KUBECTL_CONTEXT", ""),
|
||||
kubeApiProxyEnabled: envBool("V3SCTL_KUBE_API_PROXY_ENABLED", true),
|
||||
kubeconfigPath: envString("V3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/v8s/kubeconfig"),
|
||||
kubeApiConnectHost: envString("V3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"),
|
||||
requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("V3SCTL_REQUEST_TIMEOUT_MS", 30_000))),
|
||||
healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("V3SCTL_HEALTH_TIMEOUT_MS", 2500))),
|
||||
clusterId: envString("K3SCTL_CLUSTER_ID", "unidesk-k3s"),
|
||||
nodeId: envString("K3SCTL_NODE_ID", "D601"),
|
||||
kubectlEnabled: envBool("K3SCTL_KUBECTL_ENABLED", false),
|
||||
kubectlContext: envString("K3SCTL_KUBECTL_CONTEXT", ""),
|
||||
kubeApiProxyEnabled: envBool("K3SCTL_KUBE_API_PROXY_ENABLED", true),
|
||||
kubeconfigPath: envString("K3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/k3s/kubeconfig"),
|
||||
kubeApiConnectHost: envString("K3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"),
|
||||
requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("K3SCTL_REQUEST_TIMEOUT_MS", 30_000))),
|
||||
healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("K3SCTL_HEALTH_TIMEOUT_MS", 2500))),
|
||||
services: mergeServices([...manifestServices, ...inlineServices]),
|
||||
};
|
||||
}
|
||||
|
||||
function log(level: "debug" | "info" | "warn" | "error", event: string, detail: JsonRecord = {}): void {
|
||||
const record: JsonRecord = { at: new Date().toISOString(), service: "v3sctl-adapter", level, event, ...detail };
|
||||
const record: JsonRecord = { at: new Date().toISOString(), service: "k3sctl-adapter", level, event, ...detail };
|
||||
recentLogs.push(record);
|
||||
while (recentLogs.length > 500) recentLogs.shift();
|
||||
try {
|
||||
@@ -304,7 +304,7 @@ function loadKubeApiClient(): KubeApiClient | null {
|
||||
const cert = kubeconfigScalar(raw, "client-certificate-data");
|
||||
const key = kubeconfigScalar(raw, "client-key-data");
|
||||
if (server.length === 0 || ca.length === 0 || cert.length === 0 || key.length === 0) throw new Error("kubeconfig must include server, CA, client certificate, and client key data");
|
||||
const dir = join(tmpdir(), `unidesk-v3sctl-kube-${config.clusterId}`);
|
||||
const dir = join(tmpdir(), `unidesk-k3sctl-kube-${config.clusterId}`);
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const client: KubeApiClient = {
|
||||
serverUrl: new URL(server),
|
||||
@@ -510,7 +510,7 @@ async function kubeApiProxyResponse(
|
||||
headers: {
|
||||
"content-type": parsed.contentType,
|
||||
"x-unidesk-proxy-mode": "kubernetes-api-service-proxy",
|
||||
"x-unidesk-v3s-service": service.id,
|
||||
"x-unidesk-k3s-service": service.id,
|
||||
"x-unidesk-response-truncated": "false",
|
||||
},
|
||||
});
|
||||
@@ -577,7 +577,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
|
||||
const response = active
|
||||
? await kubeApiServiceProxyResponse(
|
||||
service,
|
||||
new Request("http://v3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
|
||||
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
|
||||
endpoint.healthPath,
|
||||
"",
|
||||
config.healthTimeoutMs,
|
||||
@@ -585,7 +585,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
|
||||
: await kubeApiEndpointProxyResponse(
|
||||
service,
|
||||
endpoint,
|
||||
new Request("http://v3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
|
||||
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
|
||||
endpoint.healthPath,
|
||||
"",
|
||||
config.healthTimeoutMs,
|
||||
@@ -650,7 +650,7 @@ async function probeKubernetesPodReady(service: ManagedService, endpoint: Manage
|
||||
}).toString();
|
||||
const response = await kubeApiProxyResponse(
|
||||
service,
|
||||
new Request("http://v3sctl-adapter.local/api/pods", { method: "GET", headers: { accept: "application/json" } }),
|
||||
new Request("http://k3sctl-adapter.local/api/pods", { method: "GET", headers: { accept: "application/json" } }),
|
||||
`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods`,
|
||||
`?${labelSelector}`,
|
||||
config.healthTimeoutMs,
|
||||
@@ -700,7 +700,7 @@ async function serviceStatus(service: ManagedService): Promise<JsonRecord> {
|
||||
upstreamStatus: null,
|
||||
contentType: null,
|
||||
checkedAt: new Date().toISOString(),
|
||||
error: "v3s managed service route must be kubernetes-service",
|
||||
error: "k3s managed service route must be kubernetes-service",
|
||||
noFallback: true,
|
||||
}];
|
||||
const active = instances.find((item) => item.id === service.activeInstanceId) ?? null;
|
||||
@@ -754,14 +754,14 @@ async function controlPlaneSnapshot(): Promise<JsonRecord> {
|
||||
const managedServicesHealthy = services.every((service) => service.healthy === true);
|
||||
return {
|
||||
ok: true,
|
||||
service: "v3sctl-adapter",
|
||||
service: "k3sctl-adapter",
|
||||
clusterId: config.clusterId,
|
||||
nodeId: config.nodeId,
|
||||
startedAt,
|
||||
manifestPaths: config.manifestPaths,
|
||||
managedServicesHealthy,
|
||||
noFallback: true,
|
||||
runtimePath: "frontend -> backend-core -> v3sctl-adapter -> kubernetes api service proxy -> v3s service",
|
||||
runtimePath: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service",
|
||||
kubeApiProxy: {
|
||||
enabled: config.kubeApiProxyEnabled,
|
||||
configured: kubeClient !== null,
|
||||
@@ -788,8 +788,8 @@ async function proxyToService(service: ManagedService, req: Request, targetPath:
|
||||
if (isKubernetesServiceRoute(service)) {
|
||||
return kubeApiServiceProxyResponse(service, req, targetPath, query, config.requestTimeoutMs);
|
||||
}
|
||||
log("error", "v3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true });
|
||||
return jsonResponse({ ok: false, error: "v3s managed service route must be kubernetes-service", serviceId: service.id, route: service.route, noFallback: true }, 500);
|
||||
log("error", "k3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true });
|
||||
return jsonResponse({ ok: false, error: "k3s managed service route must be kubernetes-service", serviceId: service.id, route: service.route, noFallback: true }, 500);
|
||||
}
|
||||
|
||||
async function route(req: Request): Promise<Response> {
|
||||
@@ -800,7 +800,7 @@ async function route(req: Request): Promise<Response> {
|
||||
const snapshot = await controlPlaneSnapshot();
|
||||
return jsonResponse({
|
||||
ok: true,
|
||||
service: "v3sctl-adapter",
|
||||
service: "k3sctl-adapter",
|
||||
clusterId: config.clusterId,
|
||||
nodeId: config.nodeId,
|
||||
startedAt,
|
||||
@@ -1,46 +0,0 @@
|
||||
services:
|
||||
v3sctl-adapter:
|
||||
image: unidesk-v3sctl-adapter:d601
|
||||
build:
|
||||
context: ../../../..
|
||||
dockerfile: src/components/microservices/v3sctl-adapter/Dockerfile
|
||||
args:
|
||||
V3SCTL_ADAPTER_BASE_IMAGE: ${V3SCTL_ADAPTER_BASE_IMAGE:-oven/bun:1-debian}
|
||||
container_name: v3sctl-adapter
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${V3SCTL_ADAPTER_ENV_FILE:-../../../../.state/v3sctl-adapter-d601.env}
|
||||
required: false
|
||||
ports:
|
||||
- "127.0.0.1:${V3SCTL_ADAPTER_HOST_PORT:-4266}:4266"
|
||||
environment:
|
||||
HOST: "0.0.0.0"
|
||||
PORT: "4266"
|
||||
LOG_FILE: "/var/log/unidesk/v3sctl-adapter.jsonl"
|
||||
V3SCTL_CLUSTER_ID: "${V3SCTL_CLUSTER_ID:-unidesk-v8s}"
|
||||
V3SCTL_NODE_ID: "${V3SCTL_NODE_ID:-D601}"
|
||||
V3SCTL_KUBECTL_ENABLED: "${V3SCTL_KUBECTL_ENABLED:-false}"
|
||||
V3SCTL_KUBE_API_PROXY_ENABLED: "${V3SCTL_KUBE_API_PROXY_ENABLED:-true}"
|
||||
V3SCTL_KUBECONFIG_PATH: "/var/lib/unidesk/v8s/kubeconfig"
|
||||
V3SCTL_KUBE_API_CONNECT_HOST: "${V3SCTL_KUBE_API_CONNECT_HOST:-host.docker.internal}"
|
||||
V3SCTL_MANIFEST_PATHS: "${V3SCTL_MANIFEST_PATHS:-v3s/code-queue.v3s.json,v3s/mdtodo.v3s.json}"
|
||||
V3SCTL_SERVICES_JSON: "${V3SCTL_SERVICES_JSON:-[]}"
|
||||
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
|
||||
volumes:
|
||||
- ${V3SCTL_ADAPTER_LOG_DIR:-../../../../.state/v3sctl-adapter/logs}:/var/log/unidesk
|
||||
- ${V3SCTL_KUBECONFIG_HOST_PATH:-../../../../.state/v8s/kubeconfig}:/var/lib/unidesk/v8s/kubeconfig:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- default
|
||||
- provider-gateway
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS --max-time 2 http://127.0.0.1:4266/health >/dev/null"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
networks:
|
||||
provider-gateway:
|
||||
external: true
|
||||
name: ${V3SCTL_PROVIDER_GATEWAY_NETWORK:-unidesk-provider-d601_default}
|
||||
@@ -6,7 +6,7 @@
|
||||
{ "path": "components/provider-gateway" },
|
||||
{ "path": "components/frontend" },
|
||||
{ "path": "components/microservices/code-queue" },
|
||||
{ "path": "components/microservices/v3sctl-adapter" },
|
||||
{ "path": "components/microservices/k3sctl-adapter" },
|
||||
{ "path": "components/microservices/mdtodo" },
|
||||
{ "path": "components/microservices/project-manager" },
|
||||
{ "path": "components/microservices/baidu-netdisk" }
|
||||
|
||||
Reference in New Issue
Block a user