171 lines
7.6 KiB
TypeScript
171 lines
7.6 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. policy module for scripts/src/platform-infra.ts.
|
|
|
|
// Moved mechanically from scripts/src/platform-infra.ts:1853-1973 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 { Sub2ApiConfig, Sub2ApiTargetConfig } from "./entry";
|
|
import type { DisclosureOptions, PolicyCheck } from "./options";
|
|
import { readSub2ApiConfig } from "./config";
|
|
import { serviceName } from "./entry";
|
|
import { isExternalTarget, resolveTarget } from "./manifest";
|
|
import { escapeRegExp, hasAllowAllNetworkPolicy, hasDeploymentReplicas } from "./secrets-and-egress";
|
|
import { boolField, validateExternalPendingScript } from "./utils";
|
|
import { validateExternalActiveScript, validateScript } from "./validate-script";
|
|
|
|
export async function validate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
|
const sub2api = readSub2ApiConfig();
|
|
const target = resolveTarget(sub2api, options.targetId);
|
|
const script = target.databaseMode === "external-pending"
|
|
? validateExternalPendingScript(sub2api, target)
|
|
: target.databaseMode === "external-active"
|
|
? validateExternalActiveScript(sub2api, target)
|
|
: validateScript(sub2api, target);
|
|
const result = await capture(config, target.route, ["sh"], script);
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-validate",
|
|
target: {
|
|
id: target.id,
|
|
route: target.route,
|
|
namespace: target.namespace,
|
|
},
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-validate",
|
|
target: {
|
|
id: target.id,
|
|
route: target.route,
|
|
namespace: target.namespace,
|
|
},
|
|
summary: parsed,
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
export function policyChecks(sub2api: Sub2ApiConfig, yaml: string, target: Sub2ApiTargetConfig): PolicyCheck[] {
|
|
const cleanup = sub2api.defaults.cleanup;
|
|
const redisService = sub2api.runtime.redis.serviceName;
|
|
const checks: PolicyCheck[] = [
|
|
{
|
|
name: "no-ingress",
|
|
ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml),
|
|
detail: "Sub2API must not be exposed through Kubernetes Ingress.",
|
|
},
|
|
{
|
|
name: "no-nodeport-or-loadbalancer",
|
|
ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml),
|
|
detail: "Services must remain ClusterIP/internal-only.",
|
|
},
|
|
{
|
|
name: "no-host-network",
|
|
ok: hostNetworkPolicyOk(yaml, target),
|
|
detail: target.egressProxy?.hostNetwork === true
|
|
? "Only the YAML-declared egress proxy Deployment may join the host network."
|
|
: "Pods must not join the host network unless explicitly allowed by YAML for the egress proxy.",
|
|
},
|
|
{
|
|
name: "no-host-port",
|
|
ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml),
|
|
detail: "Pods must not expose host ports.",
|
|
},
|
|
{
|
|
name: "no-cpu-memory-resources",
|
|
ok: !/^\s*(cpu|memory):\s*/mu.test(yaml),
|
|
detail: "Issue #220 requires no Kubernetes CPU/memory requests or limits.",
|
|
},
|
|
{
|
|
name: "no-resource-quota-or-limit-range",
|
|
ok: !/^\s*kind:\s*(ResourceQuota|LimitRange)\s*$/mu.test(yaml),
|
|
detail: "The platform-infra namespace must not receive quota/default limit objects for this deployment.",
|
|
},
|
|
{
|
|
name: "expected-namespace",
|
|
ok: new RegExp(`^\\s*name:\\s*${escapeRegExp(target.namespace)}\\s*$`, "mu").test(yaml),
|
|
detail: `Manifest declares namespace ${target.namespace}.`,
|
|
},
|
|
{
|
|
name: "allow-all-network-policy",
|
|
ok: hasAllowAllNetworkPolicy(yaml, target.namespace),
|
|
detail: `Manifest must include NetworkPolicy/allow-all in ${target.namespace} to keep kube-router from blocking Sub2API cross-pod traffic.`,
|
|
},
|
|
];
|
|
|
|
if (isExternalTarget(target)) {
|
|
checks.push(
|
|
{
|
|
name: "external-db-no-local-postgres",
|
|
ok: !/^\s*kind:\s*StatefulSet\s*$/mu.test(yaml) && !new RegExp(`\\b${escapeRegExp(cleanup.externalDbState.postgresStatefulSetName)}\\b`, "u").test(yaml),
|
|
detail: "External DB targets must not deploy a local PostgreSQL StatefulSet, Service, or PVC.",
|
|
});
|
|
}
|
|
|
|
if (target.databaseMode === "external-pending") {
|
|
checks.push(
|
|
{
|
|
name: "pending-db-replicas-match-yaml",
|
|
ok: hasDeploymentReplicas(yaml, serviceName, target.appReplicas) && hasDeploymentReplicas(yaml, redisService, target.redisReplicas),
|
|
detail: `External-pending predeployment renders YAML-declared app/Redis replicas ${target.appReplicas}/${target.redisReplicas}.`,
|
|
},
|
|
);
|
|
} else if (target.databaseMode === "external-active") {
|
|
checks.push({
|
|
name: "external-active-replicas-match-yaml",
|
|
ok: hasDeploymentReplicas(yaml, serviceName, target.appReplicas) && hasDeploymentReplicas(yaml, redisService, target.redisReplicas),
|
|
detail: `External-active targets render YAML-declared app/Redis replicas ${target.appReplicas}/${target.redisReplicas} against the external PostgreSQL runtime.`,
|
|
});
|
|
} else {
|
|
checks.push({
|
|
name: "bundled-db-present",
|
|
ok: /^\s*kind:\s*StatefulSet\s*$/mu.test(yaml) && new RegExp(`\\b${escapeRegExp(cleanup.externalDbState.postgresStatefulSetName)}\\b`, "u").test(yaml),
|
|
detail: "Bundled active targets render the local PostgreSQL StatefulSet.",
|
|
});
|
|
}
|
|
|
|
if (target.redisMode === "local-ephemeral") {
|
|
checks.push({
|
|
name: "local-redis-ephemeral",
|
|
ok: !new RegExp(`\\b${escapeRegExp(cleanup.redisPersistentState.pvcName)}\\b`, "u").test(yaml) && /^\s*emptyDir:\s*\{\}\s*$/mu.test(yaml),
|
|
detail: "Local Redis is an ephemeral cache and must not allocate persistent Redis storage.",
|
|
});
|
|
}
|
|
|
|
if (target.egressProxy?.enabled) {
|
|
checks.push({
|
|
name: "egress-proxy-yaml-controlled",
|
|
ok: hasDeploymentReplicas(yaml, target.egressProxy.deploymentName, 1) && new RegExp(`^\\s*name:\\s*${escapeRegExp(target.egressProxy.serviceName)}\\s*$`, "mu").test(yaml),
|
|
detail: "Egress HTTP proxy client must be rendered as a YAML-controlled platform-infra Deployment and ClusterIP Service.",
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
|
|
function hostNetworkPolicyOk(yaml: string, target: Sub2ApiTargetConfig): boolean {
|
|
const matches = [...yaml.matchAll(/^\s*hostNetwork:\s*true\s*$/gmu)];
|
|
if (matches.length === 0) return true;
|
|
const proxy = target.egressProxy;
|
|
if (proxy === null || !proxy.enabled || !proxy.hostNetwork || matches.length !== 1) return false;
|
|
const egressProxyDeployment = new RegExp(
|
|
`kind:\\s*Deployment[\\s\\S]*?name:\\s*${escapeRegExp(proxy.deploymentName)}[\\s\\S]*?hostNetwork:\\s*true`,
|
|
"u",
|
|
);
|
|
return egressProxyDeployment.test(yaml);
|
|
}
|