feat: 接入 Sub2Rank 自动交付链

This commit is contained in:
Codex
2026-07-13 07:52:59 +02:00
parent 8c3d4fb289
commit eed475abdc
9 changed files with 671 additions and 110 deletions
+22
View File
@@ -288,6 +288,28 @@ sourceAuthority:
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
configRef: config/cicd-branch-followers.yaml#controller.source.gitMirrorReadUrl
disposition: replaced-by-gitea
- key: sub2rank-nc01
targetId: NC01
upstream:
repository: pikasTech/sub2rank
cloneUrl: https://github.com/pikasTech/sub2rank.git
branch: master
gitea:
owner: mirrors
name: pikasTech-sub2rank
mirrorMode: controlled-push
publicRead: true
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
gitops:
branch: master
flushDisposition: not-a-gitops-branch
snapshot:
naming: application-source-snapshot
prefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
legacyGitMirror:
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
configRef: config/platform-infra/sub2rank.yaml#application.remote
disposition: replaced-by-gitea
- key: hwlab-jd01-v03
targetId: JD01
upstream:
@@ -121,6 +121,38 @@ repositories:
unidesk_host_runtime_service_port: "4288"
unidesk_host_health_path: /health
unidesk_host_health_url: http://todo-note.unidesk.svc.cluster.local:4288/health
- id: sub2rank-nc01
name: sub2rank-nc01
namespace: devops-infra
providerType: gitea
url: https://gitea.pikapython.com/mirrors/pikasTech-sub2rank
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
owner: mirrors
repo: pikasTech-sub2rank
secretName: pac-gitea-sub2rank-nc01
tokenKey: token
webhookSecretKey: webhook.secret
concurrencyLimit: 1
params:
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
source_branch: master
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
node: NC01
image_repository: 127.0.0.1:5000/sub2rank/sub2rank
registry_probe_base: http://127.0.0.1:5000
gitops_branch: unidesk-host-gitops
gitops_manifest_path: deploy/gitops/platform-infra/sub2rank-nc01/resources.yaml
pipeline_name: platform-infra-sub2rank-nc01-pac
pipeline_run_prefix: sub2rank-nc01
service_account: default
pipeline_timeout: 600s
workspace_pvc_size: 4Gi
runtime_namespace: platform-infra
runtime_deployment: sub2rank
runtime_service: sub2rank
runtime_service_port: "8080"
health_path: /health
health_url: http://sub2rank.platform-infra.svc.cluster.local:8080/health
- extends: templates.repositories.hwlabV03
variables:
NODE: JD01
@@ -192,6 +224,41 @@ consumers:
argoNamespace: argocd
argoApplication: platform-infra-gitea-nc01
closeoutGitOpsMirrorFlush: false
- id: platform-infra-sub2rank-nc01
repositoryRef: sub2rank-nc01
node: NC01
lane: platform-infra
namespace: devops-infra
pipeline: platform-infra-sub2rank-nc01-pac
pipelineRunPrefix: sub2rank-nc01
argoNamespace: argocd
argoApplication: sub2rank-nc01
closeoutGitOpsMirrorFlush: true
closeoutGitOpsMirrorLane: v03
argoBootstrap:
project: default
repoUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
targetRevision: unidesk-host-gitops
path: deploy/gitops/platform-infra/sub2rank-nc01
destinationNamespace: platform-infra
automated: true
deliveryProvenance:
required: true
markerValue: admission-pac-v2:platform-infra-sub2rank-nc01
executionServiceAccountName: default
gitOps:
repoUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
targetRevision: unidesk-host-gitops
sourceArtifact:
mode: embedded-pipeline-spec
renderer: sub2rank-platform-service
configRef: config/platform-infra/sub2rank.yaml#delivery
pipelineRunPath: .tekton/sub2rank-nc01-pac.yaml
maxKeepRuns: 8
taskRunTemplate:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
fsGroup: 1000
- extends: templates.consumers.hwlabV03
variables:
NODE: JD01
+52 -1
View File
@@ -10,15 +10,66 @@ defaults:
targetId: NC01
application:
repository: pikasTech/sub2rank
remote: https://github.com/pikasTech/sub2rank.git
branch: master
sourceRoot: /root/sub2rank
configRef: /root/sub2rank/config/sub2rank.yaml
sourceConfigPath: config/sub2rank.yaml
dockerfile: Dockerfile
runtimeTarget: k8s
image:
repository: 127.0.0.1:5000/sub2rank/sub2rank
tag: 14f51f5e1242d0e048f51bbea5937e3022361e9a
pullPolicy: IfNotPresent
delivery:
enabled: true
pipeline:
namespace: devops-infra
name: platform-infra-sub2rank-nc01-pac
runPrefix: sub2rank-nc01
serviceAccountName: default
timeout: 600s
workspaceSize: 4Gi
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
buildkitImage: 127.0.0.1:5000/hwlab/buildkit:rootless
sourceSnapshotPrefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
build:
networkMode: host
proxy:
http: http://127.0.0.1:10808
https: http://127.0.0.1:10808
all: http://127.0.0.1:10808
noProxy:
- localhost
- 127.0.0.1
- "::1"
- 127.0.0.1:5000
- localhost:5000
- .svc
- .svc.cluster.local
- .cluster.local
- hyueapi.com
- .hyueapi.com
gitops:
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
branch: unidesk-host-gitops
manifestPath: deploy/gitops/platform-infra/sub2rank-nc01/resources.yaml
releaseStatePath: deploy/gitops-state/platform-infra/sub2rank-nc01.json
maxPushAttempts: 3
author:
name: UniDesk Sub2Rank CI
email: sub2rank-ci@unidesk.local
application:
name: sub2rank-nc01
namespace: argocd
project: default
path: deploy/gitops/platform-infra/sub2rank-nc01
destinationNamespace: platform-infra
automated: true
targets:
- id: NC01
route: NC01:k3s
@@ -78,12 +78,19 @@ test("owning YAML renders one child Application and the complete durable bridge
"Deployment",
"Role",
"RoleBinding",
"Role",
"RoleBinding",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionPolicyBinding",
"ServiceAccount",
"ConfigMap",
"Deployment",
]);
const pacReadOnlyNamespaces = documents
.filter((item) => item.kind === "Role" && item.metadata?.name === "unidesk-pac-consumer-runner")
.map((item) => item.metadata.namespace)
.sort();
expect(pacReadOnlyNamespaces).toEqual(["agentrun-ci", "devops-infra"]);
const candidate = documents.find((item) => item.kind === "ConfigMap" && item.metadata?.name === "gitea-github-sync-candidate");
const activeConfig = documents.find((item) => item.kind === "ConfigMap" && item.metadata?.name === "gitea-github-sync-config");
const bridge = documents.find((item) => item.kind === "Deployment" && item.metadata?.name === "gitea-github-sync");
@@ -17,9 +17,10 @@ import type { RenderedCliResult } from "./output";
import { renderedCliResult } from "./agentrun/render";
import { stableJsonSha256, stableJsonValue } from "./stable-json";
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline";
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane";
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service";
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
@@ -441,7 +442,9 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
const sourceArtifact = binding.consumer.sourceArtifact;
const rendered = sourceArtifact.renderer === "agentrun-control-plane"
? renderAgentRunDesiredPipeline(binding)
: renderHwlabDesiredPipeline(binding, sourceWorktree);
: sourceArtifact.renderer === "hwlab-runtime-lane"
? renderHwlabDesiredPipeline(binding, sourceWorktree)
: renderSub2RankDesiredPipeline(binding);
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
? rendered.pipeline
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
@@ -581,7 +584,7 @@ function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Rec
name: `${binding.consumer.pipelineRunPrefix}-{{ revision }}`,
namespace: binding.consumer.namespace,
annotations: pipelineRunAnnotations(binding, provenance),
labels: pipelineRunLabels(binding, "agentrun"),
labels: pipelineRunLabels(binding, provenance.renderer === "sub2rank-platform-service" ? "sub2rank" : "agentrun"),
},
spec: {
pipelineSpec: desiredSpec,
@@ -647,7 +650,7 @@ function pipelineRunAnnotations(binding: PacSourceArtifactBinding, provenance: P
};
}
function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun" | "hwlab"): Record<string, string> {
function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun" | "hwlab" | "sub2rank"): Record<string, string> {
return partOf === "agentrun"
? {
"app.kubernetes.io/part-of": "agentrun",
@@ -656,13 +659,19 @@ function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun"
"agentrun.pikastech.local/source-commit": "{{ revision }}",
"agentrun.pikastech.local/trigger": "pipelines-as-code",
}
: {
: partOf === "hwlab" ? {
"app.kubernetes.io/name": `${binding.consumer.id}-pac`,
"app.kubernetes.io/part-of": "hwlab",
"unidesk.ai/source-commit": "{{ revision }}",
"hwlab.pikastech.local/gitops-target": binding.consumer.lane,
"hwlab.pikastech.local/source-commit": "{{ revision }}",
"hwlab.pikastech.local/trigger": "pipelines-as-code",
} : {
"app.kubernetes.io/name": "sub2rank",
"app.kubernetes.io/part-of": "platform-infra",
"unidesk.ai/node": binding.consumer.node,
"unidesk.ai/source-commit": "{{ revision }}",
"unidesk.ai/trigger": "pipelines-as-code",
};
}
@@ -808,6 +817,9 @@ function owningSourceRemote(binding: PacSourceArtifactBinding): string {
if (binding.consumer.sourceArtifact.renderer === "agentrun-control-plane") {
return resolveAgentRunLaneTarget({ node: binding.consumer.node, lane: binding.consumer.lane }).spec.source.worktreeRemote;
}
if (binding.consumer.sourceArtifact.renderer === "sub2rank-platform-service") {
return sub2RankOwningSourceRemote();
}
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
}
@@ -923,7 +935,7 @@ function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacS
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
const provenance = pipelineProvenanceFromManifest(manifest);
if (provenance === null) return null;
if (provenance.renderer !== "agentrun-control-plane" && provenance.renderer !== "hwlab-runtime-lane") return null;
if (provenance.renderer !== "agentrun-control-plane" && provenance.renderer !== "hwlab-runtime-lane" && provenance.renderer !== "sub2rank-platform-service") return null;
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
return provenance as PacSourceArtifactProvenance;
}
@@ -537,7 +537,7 @@ function parseConsumerDeliveryProvenance(value: Record<string, unknown>, path: s
function parseSourceArtifact(value: Record<string, unknown>, path: string): PacSourceArtifactSpec {
const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service"] as const);
const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
const taskRunTemplate = y.objectField(value, "taskRunTemplate", path);
@@ -545,6 +545,7 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
if (mode === "remote-pipeline-annotation" && pipelinePath === null) throw new Error(`${path}.pipelinePath is required for remote-pipeline-annotation`);
if (renderer === "agentrun-control-plane" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer agentrun-control-plane requires embedded-pipeline-spec`);
if (renderer === "hwlab-runtime-lane" && mode !== "remote-pipeline-annotation") throw new Error(`${path}.renderer hwlab-runtime-lane requires remote-pipeline-annotation`);
if (renderer === "sub2rank-platform-service" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer sub2rank-platform-service requires embedded-pipeline-spec`);
return {
mode,
renderer,
+174 -11
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { relative } from "node:path";
import { existsSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { rootPath } from "./config";
import type { PublicServiceExposure, PublicServiceTarget } from "./platform-infra-public-service";
import { assertNoDuplicateYamlMappingKeys, createYamlFieldReader, readYamlRecord, resolveRepoPath } from "./platform-infra-ops-library";
@@ -16,20 +16,61 @@ export interface Sub2RankConfig {
metadata: { id: string; owner: string; relatedIssues: number[] };
defaults: { targetId: string };
application: {
repository: string;
remote: string;
branch: string;
sourceRoot: string;
configRef: string;
sourceConfigPath: string;
dockerfile: string;
runtimeTarget: string;
configText: string;
configSha256: string;
sourceCommit: string;
sourceClean: boolean;
sourceRemotePresent: boolean;
sourceRemoteMatches: boolean;
configMatchesSourceCommit: boolean;
deploymentBlockPresent: boolean;
automaticCreditEnabled: boolean;
server: { listenPort: number; databasePath: string; envKeys: string[] };
};
image: { repository: string; tag: string; pullPolicy: "Always" | "IfNotPresent" | "Never" };
image: { repository: string; pullPolicy: "Always" | "IfNotPresent" | "Never" };
delivery: {
enabled: boolean;
pipeline: {
namespace: string;
name: string;
runPrefix: string;
serviceAccountName: string;
timeout: string;
workspaceSize: string;
toolsImage: string;
buildkitImage: string;
sourceSnapshotPrefix: string;
};
build: {
networkMode: "host";
proxy: { http: string; https: string; all: string; noProxy: string[] };
};
gitops: {
readUrl: string;
writeUrl: string;
branch: string;
manifestPath: string;
releaseStatePath: string;
maxPushAttempts: number;
author: { name: string; email: string };
application: {
name: string;
namespace: string;
project: string;
path: string;
destinationNamespace: string;
automated: true;
};
};
};
targets: Sub2RankTarget[];
runtime: {
sharedNetworkPolicyRef: { name: string; managedBySub2Rank: false };
@@ -82,15 +123,17 @@ export function readSub2RankConfig(): Sub2RankConfig {
const defaultsRecord = y.objectField(root, "defaults", "");
const applicationRecord = y.objectField(root, "application", "");
const imageRecord = y.objectField(root, "image", "");
const deliveryRecord = y.objectField(root, "delivery", "");
const runtimeRecord = y.objectField(root, "runtime", "");
const defaults = { targetId: simpleId(y.stringField(defaultsRecord, "targetId", "defaults"), "defaults.targetId") };
const application = parseApplication(applicationRecord);
const image = parseImage(imageRecord);
const delivery = parseDelivery(deliveryRecord);
const runtimeBase = parseRuntime(runtimeRecord);
const secret = resolveSecretDeclaration(runtimeBase.secret.configRef, runtimeBase.secret.declarationId);
const targets = parseTargets(root.targets, secret);
if (!targets.some((target) => target.id === defaults.targetId)) throw new Error(`${sub2RankConfigLabel}.targets must include defaults.targetId ${defaults.targetId}`);
validateResolvedConfig(application, image, runtimeBase, secret, targets);
validateResolvedConfig(application, image, delivery, runtimeBase, secret, targets);
return {
version,
kind: "platform-infra-sub2rank",
@@ -102,6 +145,7 @@ export function readSub2RankConfig(): Sub2RankConfig {
defaults,
application,
image,
delivery,
targets,
runtime: { ...runtimeBase, secret: { ...runtimeBase.secret, ...secret } },
};
@@ -116,10 +160,16 @@ export function resolveSub2RankTarget(config: Sub2RankConfig, targetId: string |
}
function parseApplication(record: Record<string, unknown>): Sub2RankConfig["application"] {
const repository = repositoryValue(y.stringField(record, "repository", "application"), "application.repository");
const remote = y.stringField(record, "remote", "application");
const branch = simpleId(y.stringField(record, "branch", "application"), "application.branch");
const sourceRoot = y.absolutePathField(record, "sourceRoot", "application");
const configRef = y.absolutePathField(record, "configRef", "application");
const sourceConfigPath = safeRelativePath(y.stringField(record, "sourceConfigPath", "application"), "application.sourceConfigPath");
const dockerfile = safeRelativePath(y.stringField(record, "dockerfile", "application"), "application.dockerfile");
const runtimeTarget = simpleId(y.stringField(record, "runtimeTarget", "application"), "application.runtimeTarget");
if (!configRef.startsWith(`${sourceRoot.replace(/\/+$/u, "")}/`)) throw new Error(`${sub2RankConfigLabel}.application.configRef must be inside application.sourceRoot`);
if (!existsSync(resolve(sourceRoot, dockerfile))) throw new Error(`${sub2RankConfigLabel}.application.dockerfile does not exist under application.sourceRoot`);
const configText = readFileSync(configRef, "utf8");
assertNoDuplicateYamlMappingKeys(configText, configRef);
const app = y.asRecord(Bun.YAML.parse(configText), configRef);
@@ -140,18 +190,25 @@ function parseApplication(record: Record<string, unknown>): Sub2RankConfig["appl
const sourceCommit = gitText(sourceRoot, ["rev-parse", "HEAD"], "resolve Sub2Rank source commit");
if (!/^[a-f0-9]{40}$/u.test(sourceCommit)) throw new Error(`${sourceRoot} HEAD must resolve to a full Git commit`);
const statusShort = gitText(sourceRoot, ["status", "--porcelain"], "read Sub2Rank source status", true);
const remote = gitText(sourceRoot, ["remote"], "read Sub2Rank source remotes", true);
const observedRemote = gitText(sourceRoot, ["remote", "get-url", "origin"], "read Sub2Rank origin", true);
const configRelativePath = relative(sourceRoot, configRef).replaceAll("\\", "/");
if (configRelativePath !== sourceConfigPath) throw new Error(`${sub2RankConfigLabel}.application.sourceConfigPath must resolve to application.configRef`);
const committedConfig = gitText(sourceRoot, ["show", `${sourceCommit}:${configRelativePath}`], "read committed Sub2Rank application config", true, false);
return {
repository,
remote,
branch,
sourceRoot,
configRef,
sourceConfigPath,
dockerfile,
runtimeTarget,
configText,
configSha256: createHash("sha256").update(configText).digest("hex"),
sourceCommit,
sourceClean: statusShort.length === 0,
sourceRemotePresent: remote.split(/\r?\n/u).some((value) => value.trim().length > 0),
sourceRemotePresent: observedRemote.length > 0,
sourceRemoteMatches: sameRepositoryIdentity(remote, observedRemote),
configMatchesSourceCommit: committedConfig === configText.trimEnd(),
deploymentBlockPresent: app.deployment !== undefined,
automaticCreditEnabled,
@@ -165,11 +222,66 @@ function parseApplication(record: Record<string, unknown>): Sub2RankConfig["appl
function parseImage(record: Record<string, unknown>): Sub2RankConfig["image"] {
const repository = y.stringField(record, "repository", "image");
const tag = y.stringField(record, "tag", "image");
const pullPolicy = y.enumField(record, "pullPolicy", "image", ["Always", "IfNotPresent", "Never"] as const);
if (!/^[A-Za-z0-9._:/-]+$/u.test(repository) || repository.endsWith("/")) throw new Error(`${sub2RankConfigLabel}.image.repository has an unsupported format`);
if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${sub2RankConfigLabel}.image.tag has an unsupported format`);
return { repository, tag, pullPolicy };
return { repository, pullPolicy };
}
function parseDelivery(record: Record<string, unknown>): Sub2RankConfig["delivery"] {
const pipeline = y.objectField(record, "pipeline", "delivery");
const build = y.objectField(record, "build", "delivery");
const proxy = y.objectField(build, "proxy", "delivery.build");
const gitops = y.objectField(record, "gitops", "delivery");
const author = y.objectField(gitops, "author", "delivery.gitops");
const application = y.objectField(gitops, "application", "delivery.gitops");
const timeout = y.stringField(pipeline, "timeout", "delivery.pipeline");
if (!/^[1-9][0-9]*s$/u.test(timeout)) throw new Error(`${sub2RankConfigLabel}.delivery.pipeline.timeout must be positive seconds`);
const sourceSnapshotPrefix = y.stringField(pipeline, "sourceSnapshotPrefix", "delivery.pipeline");
if (!/^refs\/[A-Za-z0-9._/-]+$/u.test(sourceSnapshotPrefix) || sourceSnapshotPrefix.includes("..")) throw new Error(`${sub2RankConfigLabel}.delivery.pipeline.sourceSnapshotPrefix must be a safe refs/ prefix`);
const email = y.stringField(author, "email", "delivery.gitops.author");
if (!/^[^@\s]+@[^@\s]+$/u.test(email)) throw new Error(`${sub2RankConfigLabel}.delivery.gitops.author.email must be an email address`);
const automated = y.booleanField(application, "automated", "delivery.gitops.application");
if (!automated) throw new Error(`${sub2RankConfigLabel}.delivery.gitops.application.automated must remain true`);
return {
enabled: y.booleanField(record, "enabled", "delivery"),
pipeline: {
namespace: y.kubernetesNameField(pipeline, "namespace", "delivery.pipeline"),
name: y.kubernetesNameField(pipeline, "name", "delivery.pipeline"),
runPrefix: y.kubernetesNameField(pipeline, "runPrefix", "delivery.pipeline"),
serviceAccountName: y.kubernetesNameField(pipeline, "serviceAccountName", "delivery.pipeline"),
timeout,
workspaceSize: storageSize(y.stringField(pipeline, "workspaceSize", "delivery.pipeline")),
toolsImage: imageValue(y.stringField(pipeline, "toolsImage", "delivery.pipeline"), "delivery.pipeline.toolsImage"),
buildkitImage: imageValue(y.stringField(pipeline, "buildkitImage", "delivery.pipeline"), "delivery.pipeline.buildkitImage"),
sourceSnapshotPrefix,
},
build: {
networkMode: y.enumField(build, "networkMode", "delivery.build", ["host"] as const),
proxy: {
http: httpUrlValue(y.stringField(proxy, "http", "delivery.build.proxy"), "delivery.build.proxy.http"),
https: httpUrlValue(y.stringField(proxy, "https", "delivery.build.proxy"), "delivery.build.proxy.https"),
all: httpUrlValue(y.stringField(proxy, "all", "delivery.build.proxy"), "delivery.build.proxy.all"),
noProxy: y.stringArrayField(proxy, "noProxy", "delivery.build.proxy"),
},
},
gitops: {
readUrl: httpUrlValue(y.stringField(gitops, "readUrl", "delivery.gitops"), "delivery.gitops.readUrl"),
writeUrl: httpUrlValue(y.stringField(gitops, "writeUrl", "delivery.gitops"), "delivery.gitops.writeUrl"),
branch: simpleId(y.stringField(gitops, "branch", "delivery.gitops"), "delivery.gitops.branch"),
manifestPath: safeRelativePath(y.stringField(gitops, "manifestPath", "delivery.gitops"), "delivery.gitops.manifestPath"),
releaseStatePath: safeRelativePath(y.stringField(gitops, "releaseStatePath", "delivery.gitops"), "delivery.gitops.releaseStatePath"),
maxPushAttempts: positiveInteger(gitops, "maxPushAttempts", "delivery.gitops"),
author: { name: y.stringField(author, "name", "delivery.gitops.author"), email },
application: {
name: y.kubernetesNameField(application, "name", "delivery.gitops.application"),
namespace: y.kubernetesNameField(application, "namespace", "delivery.gitops.application"),
project: y.kubernetesNameField(application, "project", "delivery.gitops.application"),
path: safeRelativePath(y.stringField(application, "path", "delivery.gitops.application"), "delivery.gitops.application.path"),
destinationNamespace: y.kubernetesNameField(application, "destinationNamespace", "delivery.gitops.application"),
automated: true,
},
},
};
}
function parseRuntime(record: Record<string, unknown>): Omit<Sub2RankConfig["runtime"], "secret"> & {
@@ -328,13 +440,19 @@ function resolveSecretDeclaration(configRef: string, declarationId: string): Res
function validateResolvedConfig(
application: Sub2RankConfig["application"],
image: Sub2RankConfig["image"],
delivery: Sub2RankConfig["delivery"],
runtime: ReturnType<typeof parseRuntime>,
secret: ResolvedSecretDeclaration,
targets: Sub2RankTarget[],
): void {
if (!/^[a-f0-9]{40}$/u.test(image.tag) || image.tag !== application.sourceCommit) throw new Error(`${sub2RankConfigLabel}.image.tag must equal the full application source commit ${application.sourceCommit}`);
if (!application.sourceClean) throw new Error(`${sub2RankConfigLabel}.application.sourceRoot must be clean before rendering deployment config`);
if (!application.configMatchesSourceCommit) throw new Error(`${sub2RankConfigLabel}.application.configRef must match the file stored at application source commit ${application.sourceCommit}`);
if (!application.sourceRemoteMatches) throw new Error(`${sub2RankConfigLabel}.application.remote must identify the application.sourceRoot origin repository`);
if (!sameRepositoryIdentity(application.remote, `https://github.com/${application.repository}.git`)) throw new Error(`${sub2RankConfigLabel}.application.repository must match application.remote`);
if (!delivery.enabled) throw new Error(`${sub2RankConfigLabel}.delivery.enabled must remain true while Sub2Rank is deployed`);
if (delivery.gitops.maxPushAttempts > 5) throw new Error(`${sub2RankConfigLabel}.delivery.gitops.maxPushAttempts must be <= 5`);
if (dirname(delivery.gitops.manifestPath).replaceAll("\\", "/") !== delivery.gitops.application.path) throw new Error(`${sub2RankConfigLabel}.delivery.gitops.manifestPath must be directly under delivery.gitops.application.path`);
if (!delivery.build.proxy.noProxy.includes("hyueapi.com") || !delivery.build.proxy.noProxy.includes(".hyueapi.com")) throw new Error(`${sub2RankConfigLabel}.delivery.build.proxy.noProxy must retain hyueapi.com and .hyueapi.com`);
if (!image.repository.startsWith("127.0.0.1:5000/")) throw new Error(`${sub2RankConfigLabel}.image.repository must use the NC01 local registry`);
const required = new Set(runtime.secret.requiredTargetKeys);
const provided = new Set(secret.mappings.map((item) => item.targetKey));
const missing = [...required].filter((key) => !provided.has(key));
@@ -345,6 +463,7 @@ function validateResolvedConfig(
for (const target of targets) {
if (target.route !== secret.route || target.namespace !== secret.namespace) throw new Error(`${sub2RankConfigLabel}.targets[${target.id}] route/namespace must match runtime Secret target ${secret.targetId}`);
if (target.publicExposure.frpc.localPort !== runtime.service.port) throw new Error(`${sub2RankConfigLabel}.targets[${target.id}].publicExposure.frpc.localPort must match runtime.service.port`);
if (target.namespace !== delivery.gitops.application.destinationNamespace) throw new Error(`${sub2RankConfigLabel}.delivery.gitops.application.destinationNamespace must match the runtime target namespace`);
}
}
@@ -403,6 +522,50 @@ function sourceRefValue(value: unknown, path: string): string {
return text;
}
function safeRelativePath(value: string, path: string): string {
if (value.startsWith("/") || value.split(/[\\/]/u).includes("..") || !/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be a safe relative path`);
return value;
}
function repositoryValue(value: string, path: string): string {
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be owner/repository`);
return value;
}
function httpUrlValue(value: string, path: string): string {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new Error(`${sub2RankConfigLabel}.${path} must be an HTTP URL`);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${sub2RankConfigLabel}.${path} must be an HTTP URL`);
if (parsed.username.length > 0 || parsed.password.length > 0 || parsed.hash.length > 0) throw new Error(`${sub2RankConfigLabel}.${path} must not contain credentials or a fragment`);
return value;
}
function sameRepositoryIdentity(left: string, right: string): boolean {
const identity = (value: string): string | null => {
const scp = /^[^/@\s]+@([^:\s]+):(.+)$/u.exec(value.trim());
let host = scp?.[1] ?? "";
let path = scp?.[2] ?? "";
if (host.length === 0 || path.length === 0) {
try {
const parsed = new URL(value.trim());
host = parsed.hostname;
path = parsed.pathname;
} catch {
return null;
}
}
const parts = path.replace(/^\/+|\/+$/gu, "").split("/");
if (parts.length !== 2) return null;
return `${host.toLowerCase()}/${parts[0]?.toLowerCase()}/${parts[1]?.replace(/\.git$/iu, "").toLowerCase()}`;
};
const leftIdentity = identity(left);
return leftIdentity !== null && leftIdentity === identity(right);
}
function kubernetesNameValue(value: unknown, path: string): string {
const text = stringValue(value, path);
if (!/^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/u.test(text)) throw new Error(`${path} must be a Kubernetes name`);
@@ -0,0 +1,256 @@
import { stableJsonSha256 } from "./stable-json";
import {
readSub2RankConfig,
resolveSub2RankTarget,
sub2RankConfigLabel,
type Sub2RankConfig,
type Sub2RankTarget,
} from "./platform-infra-sub2rank-config";
import {
renderSub2RankManifest,
sub2RankConfigBase64Placeholder,
sub2RankConfigSha256Placeholder,
sub2RankImagePlaceholder,
sub2RankSourceCommitPlaceholder,
} from "./platform-infra-sub2rank";
import type { PacSourceArtifactBinding, PacSourceArtifactMode, PacSourceArtifactRenderer } from "./platform-infra-pipelines-as-code-source-artifact";
export interface Sub2RankPipelineProvenance {
readonly configRef: string;
readonly effectiveConfigSha256: string;
readonly renderer: PacSourceArtifactRenderer;
readonly mode: PacSourceArtifactMode;
}
export function sub2RankOwningSourceRemote(): string {
return readSub2RankConfig().application.remote;
}
export function renderSub2RankDesiredPipeline(binding: PacSourceArtifactBinding): {
pipeline: Record<string, unknown>;
provenance: Sub2RankPipelineProvenance;
} {
const config = readSub2RankConfig();
const target = resolveSub2RankTarget(config, binding.consumer.node);
const expectedConfigRef = `${sub2RankConfigLabel}#delivery`;
if (binding.consumer.sourceArtifact.configRef !== expectedConfigRef) throw new Error(`Sub2Rank sourceArtifact.configRef must equal ${expectedConfigRef}`);
assertBinding(config, target, binding);
const manifestTemplate = renderSub2RankManifest(config, target, {
imageRef: sub2RankImagePlaceholder,
appConfigBase64: sub2RankConfigBase64Placeholder,
appConfigSha256: sub2RankConfigSha256Placeholder,
sourceCommit: sub2RankSourceCommitPlaceholder,
});
return {
pipeline: renderPipeline(config, target, binding, manifestTemplate),
provenance: {
configRef: expectedConfigRef,
effectiveConfigSha256: stableJsonSha256(pipelineOwner(config, target)),
renderer: "sub2rank-platform-service",
mode: "embedded-pipeline-spec",
},
};
}
function renderPipeline(
config: Sub2RankConfig,
target: Sub2RankTarget,
binding: PacSourceArtifactBinding,
manifestTemplate: string,
): Record<string, unknown> {
const delivery = config.delivery;
const params = [
{ name: "git-read-url", type: "string", default: binding.repository.cloneUrl },
{ name: "source-branch", type: "string", default: config.application.branch },
{ name: "revision", type: "string" },
{ name: "source-stage-ref", type: "string" },
{ name: "config-path", type: "string", default: config.application.sourceConfigPath },
{ name: "dockerfile", type: "string", default: config.application.dockerfile },
{ name: "image-repository", type: "string", default: config.image.repository },
{ name: "tools-image", type: "string", default: delivery.pipeline.toolsImage },
{ name: "buildkit-image", type: "string", default: delivery.pipeline.buildkitImage },
{ name: "build-network", type: "string", default: delivery.build.networkMode },
{ name: "build-http-proxy", type: "string", default: delivery.build.proxy.http },
{ name: "build-https-proxy", type: "string", default: delivery.build.proxy.https },
{ name: "build-all-proxy", type: "string", default: delivery.build.proxy.all },
{ name: "build-no-proxy", type: "string", default: delivery.build.proxy.noProxy.join(",") },
{ name: "gitops-read-url", type: "string", default: delivery.gitops.readUrl },
{ name: "gitops-write-url", type: "string", default: delivery.gitops.writeUrl },
{ name: "gitops-branch", type: "string", default: delivery.gitops.branch },
{ name: "gitops-manifest-path", type: "string", default: delivery.gitops.manifestPath },
{ name: "gitops-release-state-path", type: "string", default: delivery.gitops.releaseStatePath },
{ name: "gitops-max-push-attempts", type: "string", default: String(delivery.gitops.maxPushAttempts) },
{ name: "gitops-author-name", type: "string", default: delivery.gitops.author.name },
{ name: "gitops-author-email", type: "string", default: delivery.gitops.author.email },
{ name: "manifest-template-b64", type: "string", default: Buffer.from(manifestTemplate, "utf8").toString("base64") },
];
const task = (name: string, steps: readonly Record<string, unknown>[], runAfter: readonly string[] = []): Record<string, unknown> => ({
name,
...(runAfter.length === 0 ? {} : { runAfter }),
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: { params: params.map(({ name }) => ({ name })), workspaces: [{ name: "source" }], steps },
params: params.map(({ name: paramName }) => ({ name: paramName, value: `$(params.${paramName})` })),
});
return {
apiVersion: "tekton.dev/v1",
kind: "Pipeline",
metadata: {
name: delivery.pipeline.name,
namespace: delivery.pipeline.namespace,
labels: {
"app.kubernetes.io/name": "sub2rank",
"app.kubernetes.io/part-of": "platform-infra",
"app.kubernetes.io/managed-by": "unidesk",
"unidesk.ai/node": target.id,
},
},
spec: {
params,
workspaces: [{ name: "source" }],
tasks: [
task("source-validate", [{
name: "checkout-and-cli-validate",
image: "$(params.tools-image)",
imagePullPolicy: "IfNotPresent",
env: proxyEnv(),
script: sourceValidateScript(),
}]),
task("image-build", [{
name: "build-and-push",
image: "$(params.buildkit-image)",
imagePullPolicy: "IfNotPresent",
env: [
...proxyEnv(),
{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" },
{ name: "SOURCE_ROOT", value: "$(workspaces.source.path)/repo" },
{ name: "SOURCE_COMMIT", value: "$(params.revision)" },
{ name: "IMAGE_REPOSITORY", value: "$(params.image-repository)" },
{ name: "DOCKERFILE", value: "$(params.dockerfile)" },
{ name: "BUILD_NETWORK", value: "$(params.build-network)" },
{ name: "BUILD_METADATA_FILE", value: "$(workspaces.source.path)/build-metadata.json" },
{ name: "BUILD_RESULT_FILE", value: "$(workspaces.source.path)/build-result.json" },
],
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
script: buildScript(),
}], ["source-validate"]),
task("gitops-publish", [{
name: "publish-digest-manifest",
image: "$(params.tools-image)",
imagePullPolicy: "IfNotPresent",
env: proxyEnv(),
script: gitopsPublishScript(),
}], ["image-build"]),
],
},
};
}
function sourceValidateScript(): string {
return [
"#!/bin/sh",
"set -eu",
"root=\"$(workspaces.source.path)\"",
"rm -rf \"$root/repo\"",
"git clone --filter=blob:none --no-checkout \"$(params.git-read-url)\" \"$root/repo\"",
"cd \"$root/repo\"",
"git fetch --depth=1 --filter=blob:none origin \"+$(params.source-stage-ref):refs/remotes/origin/sub2rank-source-snapshot\"",
"git checkout --detach \"$(params.revision)\"",
"test \"$(git rev-parse HEAD)\" = \"$(params.revision)\"",
"bun install --frozen-lockfile",
"bun scripts/sub2rank-cli.ts --config \"$(params.config-path)\" config validate",
"chmod -R a+rX,g+rwX \"$root/repo\"",
"printf '{\"ok\":true,\"phase\":\"source-validate\",\"sourceCommit\":\"%s\",\"configPath\":\"%s\",\"valuesPrinted\":false}\\n' \"$(params.revision)\" \"$(params.config-path)\"",
].join("\n");
}
function buildScript(): string {
return [
"#!/bin/sh",
"set -eu",
"exec \"$(workspaces.source.path)/repo/scripts/ci/build-image.sh\"",
].join("\n");
}
function gitopsPublishScript(): string {
return [
"#!/bin/sh",
"set -eu",
"root=\"$(workspaces.source.path)\"",
"exec bun \"$root/repo/scripts/ci/publish-gitops.mjs\" \\",
" --source-root \"$root/repo\" \\",
" --source-commit \"$(params.revision)\" \\",
" --config-path \"$(params.config-path)\" \\",
" --metadata \"$root/build-metadata.json\" \\",
" --manifest-template-b64 \"$(params.manifest-template-b64)\" \\",
" --image-repository \"$(params.image-repository)\" \\",
" --gitops-read-url \"$(params.gitops-read-url)\" \\",
" --gitops-write-url \"$(params.gitops-write-url)\" \\",
" --gitops-branch \"$(params.gitops-branch)\" \\",
" --manifest-path \"$(params.gitops-manifest-path)\" \\",
" --release-state-path \"$(params.gitops-release-state-path)\" \\",
" --max-push-attempts \"$(params.gitops-max-push-attempts)\" \\",
" --author-name \"$(params.gitops-author-name)\" \\",
" --author-email \"$(params.gitops-author-email)\" \\",
" --worktree \"$root/gitops\"",
].join("\n");
}
function proxyEnv(): Record<string, unknown>[] {
return [
{ name: "HTTP_PROXY", value: "$(params.build-http-proxy)" },
{ name: "http_proxy", value: "$(params.build-http-proxy)" },
{ name: "HTTPS_PROXY", value: "$(params.build-https-proxy)" },
{ name: "https_proxy", value: "$(params.build-https-proxy)" },
{ name: "ALL_PROXY", value: "$(params.build-all-proxy)" },
{ name: "all_proxy", value: "$(params.build-all-proxy)" },
{ name: "NO_PROXY", value: "$(params.build-no-proxy)" },
{ name: "no_proxy", value: "$(params.build-no-proxy)" },
];
}
function assertBinding(config: Sub2RankConfig, target: Sub2RankTarget, binding: PacSourceArtifactBinding): void {
const delivery = config.delivery;
const expected: Readonly<Record<string, string>> = {
node: target.id,
lane: "platform-infra",
namespace: delivery.pipeline.namespace,
pipeline: delivery.pipeline.name,
pipelineRunPrefix: delivery.pipeline.runPrefix,
service_account: delivery.pipeline.serviceAccountName,
source_branch: config.application.branch,
source_snapshot_prefix: delivery.pipeline.sourceSnapshotPrefix,
image_repository: config.image.repository,
gitops_branch: delivery.gitops.branch,
gitops_manifest_path: delivery.gitops.manifestPath,
pipeline_name: delivery.pipeline.name,
pipeline_run_prefix: delivery.pipeline.runPrefix,
};
const observed: Readonly<Record<string, string | undefined>> = {
node: binding.consumer.node,
lane: binding.consumer.lane,
namespace: binding.consumer.namespace,
pipeline: binding.consumer.pipeline,
pipelineRunPrefix: binding.consumer.pipelineRunPrefix,
...binding.repository.params,
};
for (const [key, value] of Object.entries(expected)) {
if (observed[key] !== value) throw new Error(`Sub2Rank PaC binding ${key}=${String(observed[key])} does not match owning YAML ${value}`);
}
}
function pipelineOwner(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
return {
application: {
repository: config.application.repository,
remote: config.application.remote,
branch: config.application.branch,
sourceConfigPath: config.application.sourceConfigPath,
dockerfile: config.application.dockerfile,
runtimeTarget: config.application.runtimeTarget,
},
image: config.image,
delivery: config.delivery,
target,
runtime: config.runtime,
};
}
+73 -91
View File
@@ -1,12 +1,9 @@
import { createHash } from "node:crypto";
import type { UniDeskConfig } from "./config";
import { startJob } from "./jobs";
import {
applyManifestWithExistingSecretScript,
applyPk01CaddyBlock,
capture,
compactCapture,
dryRunManifestScript,
parseJsonOutput,
publicDnsProbe,
publicHttpProbe,
@@ -20,20 +17,25 @@ import { readSub2RankConfig, resolveSub2RankTarget, sub2RankConfigLabel, type Su
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|apply|status|validate",
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 apply [--target NC01] --dry-run",
"bun scripts/cli.ts platform-infra sub2rank apply [--target NC01] --confirm",
"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: "Consumes the prebuilt image declared by owning YAML; apply never runs Docker or builds from a host worktree.",
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.",
};
}
@@ -41,7 +43,7 @@ export function sub2RankHelp(): Record<string, unknown> {
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 === "apply") return await apply(config, parseOpsApplyOptions(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() };
@@ -50,7 +52,7 @@ export async function runSub2RankCommand(config: UniDeskConfig, args: string[]):
function plan(options: OpsCommonOptions): Record<string, unknown> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
const yaml = renderManifest(sub2rank, target);
const yaml = renderSub2RankManifest(sub2rank, target);
const policy = policyChecks(sub2rank, target, yaml);
return {
ok: policy.every((item) => item.ok),
@@ -59,104 +61,58 @@ function plan(options: OpsCommonOptions): Record<string, unknown> {
config: configSummary(sub2rank, target),
policy,
artifact: {
image: `${sub2rank.image.repository}:${sub2rank.image.tag}`,
buildDisposition: "not-performed-by-apply",
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,
publication: sub2rank.application.sourceRemotePresent
? { supported: false, blocker: "remote-source-not-registered-with-controlled-ci", required: "Register the repository as a YAML-owned PaC/Tekton source authority before publishing the image." }
: { supported: false, blocker: "remote-source-authority-missing", required: "Create or select an independent Git remote source authority, then register it with the YAML-owned PaC/Tekton producer." },
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",
dryRun: `bun scripts/cli.ts platform-infra sub2rank apply --target ${target.id} --dry-run`,
apply: `bun scripts/cli.ts platform-infra sub2rank apply --target ${target.id} --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 apply(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
async function publicExposure(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
const yaml = renderManifest(sub2rank, target);
const policy = policyChecks(sub2rank, target, yaml);
if (!policy.every((item) => item.ok)) {
return { ok: false, action: "platform-infra-sub2rank-apply", mode: "policy-blocked", mutation: false, target: targetSummary(target), policy };
}
if (options.confirm && !options.wait) {
const job = startJob(
`platform_infra_sub2rank_apply_${target.id.toLowerCase()}`,
["bun", "scripts/cli.ts", "platform-infra", "sub2rank", "apply", "--target", target.id, "--confirm", "--wait"],
`Apply ${target.id} Sub2Rank manifests and reconcile the YAML-owned PK01 Caddy site through the controlled UniDesk CLI`,
);
if (options.dryRun) {
return {
ok: true,
action: "platform-infra-sub2rank-apply",
mode: "async-job",
mutation: true,
target: targetSummary(target),
job,
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
next: {
status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
runtime: `bun scripts/cli.ts platform-infra sub2rank status --target ${target.id}`,
validate: `bun scripts/cli.ts platform-infra sub2rank validate --target ${target.id}`,
},
};
}
if (options.dryRun) {
const remote = await capture(config, target.route, ["sh"], dryRunManifestScript({
yaml,
target,
fieldManager: sub2rank.runtime.rollout.fieldManager,
manifestName: serviceId,
}));
const parsed = parseJsonOutput(remote.stdout);
return {
ok: remote.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-sub2rank-apply",
action: "platform-infra-sub2rank-public-exposure",
mode: "dry-run",
mutation: false,
target: targetSummary(target),
policy,
remote: parsed ?? compactCapture(remote, { full: true }),
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 resources = runtimeResources(sub2rank, target);
const remote = await capture(config, target.route, ["sh"], applyManifestWithExistingSecretScript({
yaml,
target,
fieldManager: sub2rank.runtime.rollout.fieldManager,
manifestName: serviceId,
secretName: resources.secretName,
requiredSecretKeys: resources.requiredSecretKeys,
deploymentNames: [resources.appDeploymentName, target.publicExposure.frpc.deploymentName],
waitTimeoutSeconds: sub2rank.runtime.rollout.waitTimeoutSeconds,
}));
const parsed = parseJsonOutput(remote.stdout);
const remoteOk = remote.exitCode === 0 && parsed?.ok === true;
const caddy = remoteOk
? await applyPk01CaddyBlock(config, serviceId, target.publicExposure)
: { ok: false, mutation: false, disposition: "skipped-runtime-apply-failed" };
const caddy = await applyPk01CaddyBlock(config, serviceId, target.publicExposure);
return {
ok: remoteOk && caddy.ok === true,
action: "platform-infra-sub2rank-apply",
ok: caddy.ok === true,
action: "platform-infra-sub2rank-public-exposure",
mode: "confirmed",
mutation: true,
target: targetSummary(target),
policy,
secret: secretSummary(sub2rank),
remote: parsed ?? compactCapture(remote, { full: true }),
pk01Caddy: caddy,
next: {
status: `bun scripts/cli.ts platform-infra sub2rank status --target ${target.id}`,
validate: `bun scripts/cli.ts platform-infra sub2rank validate --target ${target.id}`,
},
next: `Merge the application PR only after the PaC repository and source snapshot chain are ready.`,
};
}
@@ -201,10 +157,20 @@ async function validate(config: UniDeskConfig, options: OpsCommonOptions): Promi
};
}
function renderManifest(config: Sub2RankConfig, target: Sub2RankTarget): string {
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 = `${config.image.repository}:${config.image.tag}`;
const configHash = createHash("sha256").update(JSON.stringify({ deployment: config, target, appConfigSha256: config.application.configSha256 })).digest("hex").slice(0, 16);
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
@@ -216,9 +182,8 @@ metadata:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
data:
${runtime.configMap.key}: |
${indent(config.application.configText, 4)}
binaryData:
${runtime.configMap.key}: ${yamlQuoted(appConfigBase64)}
---
apiVersion: v1
kind: PersistentVolumeClaim
@@ -276,15 +241,16 @@ spec:
app.kubernetes.io/name: ${runtime.service.name}
app.kubernetes.io/part-of: platform-infra
annotations:
unidesk.ai/sub2rank-config-sha256: "${config.application.configSha256}"
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: ${image}
image: ${yamlQuoted(image)}
imagePullPolicy: ${config.image.pullPolicy}
command:
${command}
@@ -357,7 +323,13 @@ function configSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<s
return {
configRefs: configRefsSummary(config),
target: targetSummary(target),
image: `${config.image.repository}:${config.image.tag}`,
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 },
@@ -383,6 +355,7 @@ function configRefsSummary(config: Sub2RankConfig): Record<string, unknown> {
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 },
@@ -421,9 +394,18 @@ function runtimeResources(config: Sub2RankConfig, target: Sub2RankTarget): Publi
};
}
function indent(value: string, spaces: number): string {
const prefix = " ".repeat(spaces);
return value.trimEnd().split("\n").map((line) => `${prefix}${line}`).join("\n");
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 {