412 lines
17 KiB
TypeScript
412 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,
|
|
warnings: sub2rank.warnings,
|
|
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: "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 {
|
|
warnings: config.warnings,
|
|
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,
|
|
sourceConfigPathMatches: config.application.sourceConfigPathMatches,
|
|
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);
|
|
}
|