374 lines
13 KiB
TypeScript
374 lines
13 KiB
TypeScript
// 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 legacySub2apiCaddyManagedMarker = "sub2api";
|
|
|
|
export function sub2apiCaddyManagedMarker(target: Sub2ApiTargetConfig): string {
|
|
return target.id.toUpperCase() === "D601" ? legacySub2apiCaddyManagedMarker : `${legacySub2apiCaddyManagedMarker}-${target.id.toLowerCase()}`;
|
|
}
|
|
|
|
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;
|
|
sourceConfigRef: string | null;
|
|
sourceFingerprint: string | null;
|
|
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",
|
|
},
|
|
};
|
|
}
|