refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/platform-infra.ts.
|
||||
|
||||
// Moved mechanically from scripts/src/platform-infra.ts:1-337 for #903.
|
||||
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join } from "node:path";
|
||||
import type { UniDeskConfig } from "../config";
|
||||
import { rootPath } from "../config";
|
||||
import { startJob } from "../jobs";
|
||||
import type { RenderedCliResult } from "../output";
|
||||
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy";
|
||||
import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service";
|
||||
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
|
||||
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
||||
|
||||
import type { SubscriptionNode } from "./pk01-public-exposure";
|
||||
import { sub2ApiHelpTargetSummary } from "./options";
|
||||
|
||||
export const serviceName = "sub2api";
|
||||
|
||||
export const fieldManager = "unidesk-platform-infra";
|
||||
|
||||
export const manifestPath = rootPath("src", "components", "platform-infra", "sub2api", "sub2api.k8s.yaml");
|
||||
|
||||
export const configPath = rootPath("config", "platform-infra", "sub2api.yaml");
|
||||
|
||||
export const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
|
||||
|
||||
export const sub2apiCaddyManagedMarker = "sub2api";
|
||||
|
||||
export const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const;
|
||||
|
||||
export type DatabaseMode = "bundled" | "external-pending" | "external-active";
|
||||
|
||||
export type RedisMode = "bundled-persistent" | "local-ephemeral";
|
||||
|
||||
export interface Sub2ApiConfig {
|
||||
version: number;
|
||||
kind: string;
|
||||
metadata: {
|
||||
id: string;
|
||||
owner: string;
|
||||
relatedIssues: number[];
|
||||
};
|
||||
defaults: Sub2ApiDefaults;
|
||||
image: {
|
||||
repository: string;
|
||||
tag: string;
|
||||
pullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
};
|
||||
dependencyImages: {
|
||||
postgres: string;
|
||||
redis: string;
|
||||
};
|
||||
security: {
|
||||
urlAllowlist: {
|
||||
enabled: boolean;
|
||||
allowInsecureHttp: boolean;
|
||||
allowPrivateHosts: boolean;
|
||||
upstreamHosts: string[];
|
||||
};
|
||||
};
|
||||
targets: Sub2ApiTargetConfig[];
|
||||
runtime: {
|
||||
database: ExternalDatabaseConfig;
|
||||
secrets: RuntimeSecretsConfig;
|
||||
redis: RuntimeRedisConfig;
|
||||
appData: {
|
||||
mode: "persistent-pvc" | "empty-dir";
|
||||
};
|
||||
sentinel: {
|
||||
mode: "singleton";
|
||||
enabledOnTargets: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Sub2ApiDefaults {
|
||||
targetId: string;
|
||||
cleanup: {
|
||||
externalDbState: {
|
||||
postgresStatefulSetName: string;
|
||||
postgresServiceName: string;
|
||||
postgresPvcName: string;
|
||||
appDataPvcName: string;
|
||||
};
|
||||
redisPersistentState: {
|
||||
pvcName: string;
|
||||
};
|
||||
publicExposure: {
|
||||
deploymentName: string;
|
||||
configMapName: string;
|
||||
secretName: string;
|
||||
};
|
||||
egressProxy: {
|
||||
deploymentName: string;
|
||||
serviceName: string;
|
||||
secretName: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Sub2ApiTargetConfig {
|
||||
id: string;
|
||||
route: string;
|
||||
namespace: string;
|
||||
role: string;
|
||||
enabled: boolean;
|
||||
databaseMode: DatabaseMode;
|
||||
redisMode: RedisMode;
|
||||
appReplicas: number;
|
||||
redisReplicas: number;
|
||||
image: Partial<Sub2ApiConfig["image"]>;
|
||||
dependencyImages: Partial<Sub2ApiConfig["dependencyImages"]>;
|
||||
publicExposure: Sub2ApiPublicExposureConfig | null;
|
||||
egressProxy: Sub2ApiEgressProxyConfig | null;
|
||||
}
|
||||
|
||||
export interface Sub2ApiPublicExposureConfig {
|
||||
enabled: boolean;
|
||||
publicBaseUrl: string;
|
||||
dns: {
|
||||
hostname: string;
|
||||
expectedA: string;
|
||||
resolvers: string[];
|
||||
};
|
||||
frpc: {
|
||||
deploymentName: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
image: string;
|
||||
serverAddr: string;
|
||||
serverPort: number;
|
||||
proxyName: string;
|
||||
remotePort: number;
|
||||
localIP: string;
|
||||
localPort: number;
|
||||
tokenSourceRef: string;
|
||||
tokenSourceKey: string;
|
||||
};
|
||||
pk01: {
|
||||
route: string;
|
||||
caddyBinaryPath: string;
|
||||
caddyDownloadUrl: string;
|
||||
caddyDownloadProxyUrl: string | null;
|
||||
caddyConfigPath: string;
|
||||
caddyServiceName: string;
|
||||
caddyStorageDir: string;
|
||||
caddyEmail: string;
|
||||
pikanodeRoot: string;
|
||||
pikanodeContainerName: string;
|
||||
pikanodeImage: string;
|
||||
pikanodeHttpHostPort: number;
|
||||
responseHeaderTimeoutSeconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Sub2ApiEgressProxyConfig {
|
||||
enabled: boolean;
|
||||
deploymentName: string;
|
||||
serviceName: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
image: string;
|
||||
imagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
listenPort: number;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceType: "subscription-url" | "master-shadowsocks";
|
||||
preferredOutbound: "vless-reality" | "hysteria2" | null;
|
||||
masterShadowsocks: Sub2ApiMasterShadowsocksConfig | null;
|
||||
noProxy: string[];
|
||||
applyToSub2Api: boolean;
|
||||
applyToSentinel: boolean;
|
||||
healthProbeUrl: string;
|
||||
}
|
||||
|
||||
export interface Sub2ApiMasterShadowsocksConfig {
|
||||
serverHost: string;
|
||||
serverPort: number;
|
||||
method: string;
|
||||
}
|
||||
|
||||
export interface ExternalDatabaseConfig {
|
||||
mode: "external";
|
||||
sourceRef: string;
|
||||
sourceKeys: {
|
||||
user: string;
|
||||
password: string;
|
||||
dbName: string;
|
||||
};
|
||||
secretName: string;
|
||||
passwordKey: string;
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
dbName: string;
|
||||
sslMode: string;
|
||||
pendingAllowed: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeSecretsConfig {
|
||||
root: string;
|
||||
appSourceRef: string;
|
||||
}
|
||||
|
||||
export interface RuntimeRedisConfig {
|
||||
serviceName: string;
|
||||
persistence: boolean;
|
||||
}
|
||||
|
||||
export interface ExternalActiveSecretMaterial {
|
||||
sourceRef: string;
|
||||
sourcePath: string;
|
||||
appSourceRef: string;
|
||||
appSourcePath: string;
|
||||
action: "create" | "update" | "none";
|
||||
fingerprint: string;
|
||||
values: Record<typeof requiredSecretKeys[number], string>;
|
||||
}
|
||||
|
||||
export interface PublicExposureSecretMaterial {
|
||||
sourceRef: string;
|
||||
sourcePath: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
fingerprint: string;
|
||||
frpcToml: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface EgressProxySecretMaterial {
|
||||
sourceRef: string;
|
||||
sourcePath: string;
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
fingerprint: string;
|
||||
sourceBytes: number;
|
||||
selectedOutbound: string;
|
||||
sourceDiagnostics: EgressProxySubscriptionDiagnostics;
|
||||
configJson: string;
|
||||
proxyUrl: string;
|
||||
noProxy: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface EgressProxySubscriptionCandidateSummary {
|
||||
sourceLine: number;
|
||||
kind: SubscriptionNode["kind"] | "shadowsocks";
|
||||
fingerprint: string;
|
||||
paramKeys: string[];
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface EgressProxySubscriptionDiagnostics {
|
||||
ok: boolean;
|
||||
sourceType: Sub2ApiEgressProxyConfig["sourceType"];
|
||||
preferredOutbound: Sub2ApiEgressProxyConfig["preferredOutbound"];
|
||||
candidateCount: number;
|
||||
supportedCount: number;
|
||||
unsupportedCount: number;
|
||||
byKind: Record<string, number>;
|
||||
selectedOutbound: string | null;
|
||||
selectedFingerprint: string | null;
|
||||
candidates: EgressProxySubscriptionCandidateSummary[];
|
||||
error?: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface ManagedResourceCleanupPlan {
|
||||
externalDbState: {
|
||||
enabled: boolean;
|
||||
postgresStatefulSetName: string;
|
||||
postgresServiceName: string;
|
||||
postgresPvcName: string;
|
||||
appDataPvcName: string;
|
||||
};
|
||||
redisPersistentState: {
|
||||
enabled: boolean;
|
||||
pvcName: string;
|
||||
};
|
||||
publicExposure: {
|
||||
enabled: boolean;
|
||||
deploymentName: string;
|
||||
configMapName: string;
|
||||
secretName: string;
|
||||
};
|
||||
egressProxy: {
|
||||
enabled: boolean;
|
||||
deploymentName: string;
|
||||
serviceName: string;
|
||||
secretName: string;
|
||||
};
|
||||
sentinel: {
|
||||
enabled: boolean;
|
||||
cronJobName: string;
|
||||
configMapName: string;
|
||||
credentialsSecretName: string;
|
||||
stateConfigMapName: string;
|
||||
serviceAccountName: string;
|
||||
roleName: string;
|
||||
roleBindingName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function platformInfraHelp(): unknown {
|
||||
const target = sub2ApiHelpTargetSummary();
|
||||
return {
|
||||
command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability ...",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
|
||||
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run",
|
||||
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api validate [--target G14|D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm",
|
||||
"bun scripts/cli.ts platform-infra langbot plan [--target G14]",
|
||||
"bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm",
|
||||
"bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra langbot logs [--target G14] [--component app|plugin-runtime|frpc|all]",
|
||||
"bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm",
|
||||
"bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots",
|
||||
"bun scripts/cli.ts platform-infra n8n plan [--target G14]",
|
||||
"bun scripts/cli.ts platform-infra n8n apply [--target G14] --confirm",
|
||||
"bun scripts/cli.ts platform-infra n8n status [--target G14] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra n8n logs [--target G14] [--component app|frpc|all]",
|
||||
"bun scripts/cli.ts platform-infra n8n validate [--target G14] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive plan [--target G14]",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive apply [--target G14] --confirm",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive status [--target G14] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive validate [--target G14] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive wcf-host-prepare --confirm",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive wcf-host-start --confirm",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive collector-image-build --confirm",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive collector-apply --confirm",
|
||||
"bun scripts/cli.ts platform-infra wechat-archive collector-status --full",
|
||||
"bun scripts/cli.ts platform-infra observability plan --target D601",
|
||||
"bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run",
|
||||
"bun scripts/cli.ts platform-infra observability apply --target D601 --confirm",
|
||||
"bun scripts/cli.ts platform-infra observability status --target D601",
|
||||
"bun scripts/cli.ts platform-infra observability validate --target D601",
|
||||
"bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <traceId> [--grep text] [--limit 40] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra observability search --target D601 --grep 'no rollout found' [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]",
|
||||
"bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --business-trace-id <trc_...> [--full|--raw]",
|
||||
],
|
||||
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows and OpenTelemetry tracing. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
|
||||
target,
|
||||
codexPool: {
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
|
||||
],
|
||||
module: "scripts/src/platform-infra-sub2api-codex.ts",
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user