Files
pikasTech-unidesk/scripts/src/platform-infra-sub2rank.ts
T
2026-07-13 07:52:59 +02:00

414 lines
17 KiB
TypeScript

import { createHash } from "node:crypto";
import type { UniDeskConfig } from "./config";
import {
applyPk01CaddyBlock,
capture,
compactCapture,
parseJsonOutput,
publicDnsProbe,
publicHttpProbe,
publicServicePolicyChecks,
publicServiceStatusScript,
renderEnvSecretFrpcManifest,
type PublicServiceRuntimeResources,
} from "./platform-infra-public-service";
import { parseOpsApplyOptions, parseOpsCommonOptions, type OpsApplyOptions, type OpsCommonOptions } from "./platform-infra-ops-library";
import { readSub2RankConfig, resolveSub2RankTarget, sub2RankConfigLabel, type Sub2RankConfig, type Sub2RankTarget } from "./platform-infra-sub2rank-config";
const serviceId = "sub2rank";
export const sub2RankImagePlaceholder = "__SUB2RANK_IMAGE_REF__";
export const sub2RankConfigBase64Placeholder = "__SUB2RANK_CONFIG_BASE64__";
export const sub2RankConfigSha256Placeholder = "__SUB2RANK_CONFIG_SHA256__";
export const sub2RankSourceCommitPlaceholder = "__SUB2RANK_SOURCE_COMMIT__";
export function sub2RankHelp(): Record<string, unknown> {
return {
command: "platform-infra sub2rank plan|public-exposure|status|validate",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2rank plan [--target NC01]",
"bun scripts/cli.ts platform-infra sub2rank public-exposure [--target NC01] --dry-run",
"bun scripts/cli.ts platform-infra sub2rank public-exposure [--target NC01] --confirm",
"bun scripts/cli.ts platform-infra sub2rank status [--target NC01] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2rank validate [--target NC01] [--full|--raw]",
],
configTruth: sub2RankConfigLabel,
applicationConfigRef: "/root/sub2rank/config/sub2rank.yaml",
artifactPolicy: "Application PR merge is the sole delivery trigger; PaC/Tekton builds the image and publishes a digest-pinned GitOps manifest.",
secretPolicy: "Resolves the existing Secret declaration from config/secrets-distribution.yaml and never reads or prints runtime Secret values.",
};
}
export async function runSub2RankCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
const [action = "plan"] = args;
if (action === "plan") return plan(parseOpsCommonOptions(args.slice(1)));
if (action === "public-exposure") return await publicExposure(config, parseOpsApplyOptions(args.slice(1)));
if (action === "status") return await status(config, parseOpsCommonOptions(args.slice(1)));
if (action === "validate") return await validate(config, parseOpsCommonOptions(args.slice(1)));
return { ok: false, error: "unsupported-platform-infra-sub2rank-command", args, help: sub2RankHelp() };
}
function plan(options: OpsCommonOptions): Record<string, unknown> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
const yaml = renderSub2RankManifest(sub2rank, target);
const policy = policyChecks(sub2rank, target, yaml);
return {
ok: policy.every((item) => item.ok),
action: "platform-infra-sub2rank-plan",
mutation: false,
config: configSummary(sub2rank, target),
policy,
artifact: {
imageRepository: sub2rank.image.repository,
buildDisposition: "pac-tekton-on-application-pr-merge",
sourceRoot: sub2rank.application.sourceRoot,
sourceCommit: sub2rank.application.sourceCommit,
sourceClean: sub2rank.application.sourceClean,
sourceRemotePresent: sub2rank.application.sourceRemotePresent,
sourceRemoteMatches: sub2rank.application.sourceRemoteMatches,
publication: {
supported: sub2rank.delivery.enabled,
authority: "GitHub PR merge -> Gitea snapshot -> PaC -> Tekton -> GitOps -> Argo",
branch: sub2rank.application.branch,
pipeline: sub2rank.delivery.pipeline.name,
},
},
topology: `client -> PK01 Caddy -> PK01 frps:${target.publicExposure.frpc.remotePort} -> ${target.id} frpc -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`,
next: {
secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope sub2rank --confirm",
publicExposureDryRun: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --dry-run`,
publicExposureApply: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm`,
status: `bun scripts/cli.ts platform-infra sub2rank status --target ${target.id}`,
validate: `bun scripts/cli.ts platform-infra sub2rank validate --target ${target.id}`,
},
};
}
async function publicExposure(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
if (options.dryRun) {
return {
ok: true,
action: "platform-infra-sub2rank-public-exposure",
mode: "dry-run",
mutation: false,
target: targetSummary(target),
exposure: {
publicBaseUrl: target.publicExposure.publicBaseUrl,
upstream: `127.0.0.1:${target.publicExposure.frpc.remotePort}`,
managedBlock: serviceId,
},
next: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm`,
};
}
const caddy = await applyPk01CaddyBlock(config, serviceId, target.publicExposure);
return {
ok: caddy.ok === true,
action: "platform-infra-sub2rank-public-exposure",
mode: "confirmed",
mutation: true,
target: targetSummary(target),
pk01Caddy: caddy,
next: `Merge the application PR only after the PaC repository and source snapshot chain are ready.`,
};
}
async function status(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
const resources = runtimeResources(sub2rank, target);
const remote = await capture(config, target.route, ["sh"], publicServiceStatusScript({
target,
resources,
healthPath: sub2rank.runtime.probes.healthPath,
servicePort: sub2rank.runtime.service.port,
}));
const parsed = parseJsonOutput(remote.stdout);
return {
ok: remote.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-sub2rank-status",
mutation: false,
target: targetSummary(target),
configRefs: configRefsSummary(sub2rank),
summary: parsed,
remote: compactCapture(remote, { full: options.full || remote.exitCode !== 0 }),
...(options.raw ? { raw: remote } : {}),
};
}
async function validate(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
const runtime = await status(config, options);
const dns = await publicDnsProbe(target.publicExposure.dns);
const publicHttps = publicHttpProbe(target.publicExposure.publicBaseUrl, sub2rank.runtime.probes.healthPath);
return {
ok: runtime.ok === true && dns.ok === true && publicHttps.ok === true,
action: "platform-infra-sub2rank-validate",
mutation: false,
target: targetSummary(target),
runtime,
dns,
publicHttps,
automaticCredit: { enabled: sub2rank.application.automaticCreditEnabled, source: sub2rank.application.configRef },
};
}
export interface Sub2RankManifestRenderOptions {
imageRef?: string;
appConfigBase64?: string;
appConfigSha256?: string;
sourceCommit?: string;
}
export function renderSub2RankManifest(config: Sub2RankConfig, target: Sub2RankTarget, options: Sub2RankManifestRenderOptions = {}): string {
const runtime = config.runtime;
const image = options.imageRef ?? `${config.image.repository}:pac-preview`;
const appConfigBase64 = options.appConfigBase64 ?? Buffer.from(config.application.configText, "utf8").toString("base64");
const appConfigSha256 = options.appConfigSha256 ?? config.application.configSha256;
const sourceCommit = options.sourceCommit ?? config.application.sourceCommit;
const configHash = createHash("sha256").update(JSON.stringify({ deployment: deploymentHashInput(config), target })).digest("hex").slice(0, 16);
const appEnv = runtime.secret.appEnvTargetKeys.map((key) => ` - name: ${key}\n valueFrom:\n secretKeyRef:\n name: ${runtime.secret.secretName}\n key: ${key}`).join("\n");
const command = runtime.command.map((value) => ` - ${yamlQuoted(value)}`).join("\n");
return `apiVersion: v1
kind: ConfigMap
metadata:
name: ${runtime.configMap.name}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
binaryData:
${runtime.configMap.key}: ${yamlQuoted(appConfigBase64)}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${runtime.storage.claimName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: ${runtime.storage.size}
---
apiVersion: v1
kind: Service
metadata:
name: ${runtime.service.name}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: ${runtime.service.name}
ports:
- name: http
port: ${runtime.service.port}
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${runtime.service.name}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
replicas: ${target.replicas}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ${runtime.service.name}
template:
metadata:
labels:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
annotations:
unidesk.ai/sub2rank-config-sha256: "${appConfigSha256}"
unidesk.ai/sub2rank-deployment-hash: "${configHash}"
unidesk.ai/source-commit: "${sourceCommit}"
unidesk.ai/public-base-url: "${target.publicExposure.publicBaseUrl}"
spec:
securityContext:
fsGroup: 1000
containers:
- name: ${runtime.service.name}
image: ${yamlQuoted(image)}
imagePullPolicy: ${config.image.pullPolicy}
command:
${command}
ports:
- name: http
containerPort: ${runtime.service.port}
env:
${appEnv}
readinessProbe:
httpGet:
path: ${runtime.probes.healthPath}
port: http
initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
periodSeconds: ${runtime.probes.periodSeconds}
timeoutSeconds: ${runtime.probes.timeoutSeconds}
failureThreshold: ${runtime.probes.failureThreshold}
livenessProbe:
httpGet:
path: ${runtime.probes.healthPath}
port: http
initialDelaySeconds: ${runtime.probes.initialDelaySeconds}
periodSeconds: ${runtime.probes.periodSeconds}
timeoutSeconds: ${runtime.probes.timeoutSeconds}
failureThreshold: ${runtime.probes.failureThreshold}
volumeMounts:
- name: app-config
mountPath: ${runtime.configMap.mountPath}
subPath: ${runtime.configMap.key}
readOnly: true
- name: data
mountPath: ${runtime.storage.mountPath}
volumes:
- name: app-config
configMap:
name: ${runtime.configMap.name}
- name: data
persistentVolumeClaim:
claimName: ${runtime.storage.claimName}
${renderEnvSecretFrpcManifest(target, {
configMapName: target.publicExposure.frpc.configMapName,
configKey: target.publicExposure.frpc.configKey,
tokenSecretName: runtime.secret.secretName,
tokenSecretKey: target.publicExposure.frpc.tokenTargetKey,
tokenEnvName: target.publicExposure.frpc.tokenEnvName,
})}`;
}
function policyChecks(config: Sub2RankConfig, target: Sub2RankTarget, yaml: string): Array<{ name: string; ok: boolean; detail: string }> {
return [
...publicServicePolicyChecks(yaml, target, serviceId, { requireAllowAllManifest: false }).map((item) => ({ name: String(item.name), ok: item.ok === true, detail: String(item.detail) })),
{
name: "shared-runtime-resources-not-owned",
ok: !/^\s*kind:\s*(?:Namespace|NetworkPolicy)\s*$/mu.test(yaml) && config.runtime.sharedNetworkPolicyRef.managedBySub2Rank === false,
detail: `Sub2Rank references ${target.namespace}/NetworkPolicy/${config.runtime.sharedNetworkPolicyRef.name} but does not apply or seize field ownership of shared runtime resources.`,
},
{
name: "deployment-owned-by-unidesk",
ok: !config.application.deploymentBlockPresent,
detail: `Deployment, image, Secret, FRP and Caddy facts belong only to ${sub2RankConfigLabel}; the application config must not contain a deployment block.`,
},
{
name: "existing-secret-source-ref",
ok: config.runtime.secret.requiredTargetKeys.every((key) => config.runtime.secret.mappings.some((mapping) => mapping.targetKey === key)),
detail: `Runtime keys resolve through ${config.runtime.secret.configRef} declaration ${config.runtime.secret.declarationId}; valuesPrinted=false.`,
},
];
}
function configSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
return {
configRefs: configRefsSummary(config),
target: targetSummary(target),
image: { repository: config.image.repository, pullPolicy: config.image.pullPolicy, authority: "PaC digest pin" },
delivery: {
pipeline: config.delivery.pipeline.name,
gitopsBranch: config.delivery.gitops.branch,
gitopsManifestPath: config.delivery.gitops.manifestPath,
argoApplication: config.delivery.gitops.application.name,
},
runtime: {
service: `${config.runtime.service.name}.${target.namespace}.svc.cluster.local:${config.runtime.service.port}`,
storage: { claimName: config.runtime.storage.claimName, size: config.runtime.storage.size, mountPath: config.runtime.storage.mountPath },
secret: secretSummary(config),
},
publicExposure: {
publicBaseUrl: target.publicExposure.publicBaseUrl,
expectedA: target.publicExposure.dns.expectedA,
remotePort: target.publicExposure.frpc.remotePort,
marker: serviceId,
},
automaticCredit: { enabled: config.application.automaticCreditEnabled, source: config.application.configRef },
};
}
function configRefsSummary(config: Sub2RankConfig): Record<string, unknown> {
return {
deployment: { path: sub2RankConfigLabel, kind: config.kind },
application: {
path: config.application.configRef,
sha256: config.application.configSha256,
deploymentBlockPresent: config.application.deploymentBlockPresent,
sourceCommit: config.application.sourceCommit,
sourceClean: config.application.sourceClean,
sourceRemotePresent: config.application.sourceRemotePresent,
sourceRemoteMatches: config.application.sourceRemoteMatches,
configMatchesSourceCommit: config.application.configMatchesSourceCommit,
},
secret: { path: config.runtime.secret.configRef, declarationId: config.runtime.secret.declarationId },
};
}
function secretSummary(config: Sub2RankConfig): Record<string, unknown> {
return {
name: config.runtime.secret.secretName,
declarationId: config.runtime.secret.declarationId,
mappings: config.runtime.secret.mappings.map((item) => ({ sourceRef: item.sourceRef, sourceKey: item.sourceKey, targetKey: item.targetKey })),
requiredTargetKeys: config.runtime.secret.requiredTargetKeys,
valuesPrinted: false,
};
}
function targetSummary(target: Sub2RankTarget): Record<string, unknown> {
return {
id: target.id,
route: target.route,
namespace: target.namespace,
replicas: target.replicas,
publicBaseUrl: target.publicExposure.publicBaseUrl,
};
}
function runtimeResources(config: Sub2RankConfig, target: Sub2RankTarget): PublicServiceRuntimeResources {
return {
appDeploymentName: config.runtime.service.name,
serviceName: config.runtime.service.name,
persistentVolumeClaimName: config.runtime.storage.claimName,
configMapName: config.runtime.configMap.name,
secretName: config.runtime.secret.secretName,
requiredSecretKeys: config.runtime.secret.requiredTargetKeys,
sharedNetworkPolicyName: config.runtime.sharedNetworkPolicyRef.name,
};
}
function deploymentHashInput(config: Sub2RankConfig): Record<string, unknown> {
return {
application: {
repository: config.application.repository,
branch: config.application.branch,
sourceConfigPath: config.application.sourceConfigPath,
dockerfile: config.application.dockerfile,
runtimeTarget: config.application.runtimeTarget,
},
image: config.image,
runtime: config.runtime,
};
}
function yamlQuoted(value: string): string {
return JSON.stringify(value);
}