Merge remote-tracking branch 'origin/master' into fix/web-probe-memory-guard

This commit is contained in:
AgentRun Codex
2026-07-13 07:31:04 +00:00
9 changed files with 186 additions and 99 deletions
@@ -94,6 +94,10 @@ function missing(reason, sourceCommit = null) {
function isAdmissionDefault(path, value) {
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
const emptyRecord = value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
if ([
"spec.workspaces.*.volumeClaimTemplate.metadata",
"spec.workspaces.*.volumeClaimTemplate.status",
].includes(normalized)) return emptyRecord;
const atTaskSpec = (suffix) => [
`tasks.*.taskSpec.${suffix}`,
`spec.tasks.*.taskSpec.${suffix}`,
@@ -110,6 +114,8 @@ function isAdmissionDefault(path, value) {
export function canonicalizePacPipelineSpec(value, path = []) {
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
const duration = canonicalTektonPipelineTimeout(path, value);
if (duration !== null) return duration;
if (value === null || typeof value !== "object") return value;
const output = {};
for (const key of Object.keys(value).sort()) {
@@ -121,6 +127,38 @@ export function canonicalizePacPipelineSpec(value, path = []) {
return output;
}
function canonicalTektonPipelineTimeout(path, value) {
if (path.join(".") !== "spec.timeouts.pipeline" || typeof value !== "string") return null;
const matchSign = /^([+-]?)(.*)$/u.exec(value);
if (matchSign === null) return null;
const source = matchSign[2] ?? "";
const units = {
h: 3_600_000_000_000n,
m: 60_000_000_000n,
s: 1_000_000_000n,
ms: 1_000_000n,
us: 1_000n,
"µs": 1_000n,
"μs": 1_000n,
ns: 1n,
};
const segment = /([0-9]+)(ns|us|µs|μs|ms|s|m|h)/uy;
let offset = 0;
let nanoseconds = 0n;
let count = 0;
while (offset < source.length) {
segment.lastIndex = offset;
const matched = segment.exec(source);
if (matched === null) return null;
nanoseconds += BigInt(matched[1] ?? "0") * (units[matched[2] ?? ""] ?? 0n);
offset = segment.lastIndex;
count += 1;
}
if (count === 0) return null;
if (matchSign[1] === "-") nanoseconds = -nanoseconds;
return `tekton-duration-ns:${nanoseconds}`;
}
export function canonicalPacPipelineSha256(value) {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}
@@ -331,6 +331,31 @@ describe("Tekton admission canonicalization", () => {
expect(canonicalizePacPipelineSpec(negative)).toEqual(negative);
expect(nativeCanonicalize(negative)).toEqual(negative);
});
test("normalizes equivalent PipelineRun timeout spellings without hiding different values", () => {
const seconds = { spec: { timeouts: { pipeline: "600s" } } };
const admitted = { spec: { timeouts: { pipeline: "10m0s" } } };
const different = { spec: { timeouts: { pipeline: "601s" } } };
const unrelated = { timeouts: { pipeline: "600s" } };
expect(canonicalizePacPipelineSpec(seconds)).toEqual(canonicalizePacPipelineSpec(admitted));
expect(nativeCanonicalize(seconds)).toEqual(nativeCanonicalize(admitted));
expect(canonicalSha256(seconds)).toBe(canonicalSha256(admitted));
expect(nativeCanonicalSha256(admitted)).toBe(canonicalSha256(seconds));
expect(firstPacSourceArtifactDrift(seconds, admitted)).toBeNull();
expect(firstPacSourceArtifactDrift(seconds, different)?.path).toBe("$.spec.timeouts.pipeline");
expect(canonicalizePacPipelineSpec(unrelated)).toEqual(unrelated);
expect(nativeCanonicalize(unrelated)).toEqual(unrelated);
});
test("removes only empty workspace claim metadata and status added by admission", () => {
const desired = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"] } } }] } };
const admitted = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { metadata: {}, spec: { accessModes: ["ReadWriteOnce"] }, status: {} } }] } };
const labeled = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { metadata: { labels: { owner: "test" } }, spec: { accessModes: ["ReadWriteOnce"] } } }] } };
expect(canonicalizePacPipelineSpec(admitted)).toEqual(canonicalizePacPipelineSpec(desired));
expect(nativeCanonicalize(admitted)).toEqual(nativeCanonicalize(desired));
expect(firstPacSourceArtifactDrift(desired, admitted)).toBeNull();
expect(firstPacSourceArtifactDrift(desired, labeled)?.path).toBe("$.spec.workspaces[0].volumeClaimTemplate.metadata");
});
});
describe("source artifact serialization", () => {
@@ -636,6 +661,9 @@ describe("runtime source-commit selection", () => {
expect(sourceArtifactRuntimeAlignment(aligned, exact, sourceCommit)).toBe("aligned");
expect(sourceArtifactRuntimeAlignment(drifted, exact, sourceCommit)).toBe("drifted");
expect(sourceArtifactRuntimeAlignment(aligned, { ...exact, clean: false }, sourceCommit)).toBe("unavailable");
const embeddedOnly = { ...structuredClone(aligned), live: { ...aligned.live, status: "missing" as const, provenanceAligned: false } };
expect(sourceArtifactRuntimeAlignment(embeddedOnly, exact, sourceCommit, "embedded-pipeline-spec")).toBe("aligned");
expect(sourceArtifactRuntimeAlignment(embeddedOnly, exact, sourceCommit, "remote-pipeline-annotation")).toBe("missing");
expect(JSON.stringify([aligned, drifted])).not.toContain("pending");
});
@@ -284,7 +284,7 @@ export async function runPacSourceArtifact(
})
: null;
const sourceOk = source.aligned && source.provenanceAligned;
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit);
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit, binding.consumer.sourceArtifact.mode);
const runtimeOk = runtimeAlignment === "aligned";
const ok = options.action === "check"
? sourceOk
@@ -326,6 +326,9 @@ export async function runPacSourceArtifact(
liveExportAllowed: false,
sourceWorktreeExplicit: true,
canonicalAdmissionDefaultsRemoved: [
"PipelineRun.spec.timeouts.pipeline=<semantic Go duration>",
"workspaces[].volumeClaimTemplate.metadata={}",
"workspaces[].volumeClaimTemplate.status={}",
"tasks[].taskSpec.metadata={}",
"tasks[].taskSpec.spec=null",
"tasks[].taskSpec.params[].type=string",
@@ -359,13 +362,14 @@ export function sourceArtifactRuntimeAlignment(
runtime: PacSourceArtifactRuntimeObservation | null,
worktree: SourceWorktreeState,
sourceCommit: string | null,
mode: PacSourceArtifactMode = "remote-pipeline-annotation",
): PacSourceArtifactRuntimeAlignment {
if (runtime === null) return "not-requested";
if (sourceCommit !== null && (!worktree.clean || worktree.matchesSourceCommit !== true)) return "unavailable";
if (runtime.live.status === "unavailable" || runtime.embedded.status === "unavailable") return "unavailable";
if (runtime.live.status === "missing" || runtime.embedded.status === "missing") return "missing";
if (runtime.live.status === "drifted" || runtime.embedded.status === "drifted") return "drifted";
if (!runtime.live.provenanceAligned || !runtime.embedded.provenanceAligned) return "drifted";
const required = mode === "embedded-pipeline-spec" ? [runtime.embedded] : [runtime.live, runtime.embedded];
if (required.some((item) => item.status === "unavailable")) return "unavailable";
if (required.some((item) => item.status === "missing")) return "missing";
if (required.some((item) => item.status === "drifted" || !item.provenanceAligned)) return "drifted";
return "aligned";
}
@@ -386,14 +390,16 @@ function sourceArtifactNext(
if (!worktree.clean || (options.sourceCommit !== null && worktree.matchesSourceCommit !== true)) {
return "Create a clean worktree at the exact GitHub merge commit, then rerun status or verify-runtime with that full source commit.";
}
if (runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
if (runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
if (runtime.live.status === "drifted" || !runtime.live.provenanceAligned) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && (runtime.live.status === "drifted" || !runtime.live.provenanceAligned)) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
if (runtime.embedded.status === "unavailable") return "Inspect the exact-commit PaC PipelineRun identity and observer access, then rerun verify-runtime.";
if (runtime.embedded.status === "missing") return "Wait for or repair the exact-commit PaC PipelineRun; do not validate a prefix-selected run.";
if (runtime.embedded.status === "drifted" || !runtime.embedded.provenanceAligned) return "Regenerate and merge the consumer source artifact, then verify the new exact-commit PipelineRun.";
return runtimeAlignment === "aligned"
? "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
? binding.consumer.sourceArtifact.mode === "embedded-pipeline-spec"
? "Exact source, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
: "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
: "Resolve the reported runtime layer and rerun verify-runtime.";
}
@@ -428,6 +434,8 @@ export function renderPacSourceArtifactResult(result: Record<string, unknown>):
export function canonicalizePacPipelineSpec(value: unknown, path: readonly (string | number)[] = []): unknown {
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
const duration = canonicalTektonPipelineTimeout(path, value);
if (duration !== null) return duration;
if (!isRecord(value)) return value;
const output: Record<string, unknown> = {};
for (const key of Object.keys(value).sort()) {
@@ -439,6 +447,38 @@ export function canonicalizePacPipelineSpec(value: unknown, path: readonly (stri
return output;
}
function canonicalTektonPipelineTimeout(path: readonly (string | number)[], value: unknown): string | null {
if (path.join(".") !== "spec.timeouts.pipeline" || typeof value !== "string") return null;
const matchSign = /^([+-]?)(.*)$/u.exec(value);
if (matchSign === null) return null;
const source = matchSign[2] ?? "";
const units: Readonly<Record<string, bigint>> = {
h: 3_600_000_000_000n,
m: 60_000_000_000n,
s: 1_000_000_000n,
ms: 1_000_000n,
us: 1_000n,
"µs": 1_000n,
"μs": 1_000n,
ns: 1n,
};
const segment = /([0-9]+)(ns|us|µs|μs|ms|s|m|h)/uy;
let offset = 0;
let nanoseconds = 0n;
let count = 0;
while (offset < source.length) {
segment.lastIndex = offset;
const matched = segment.exec(source);
if (matched === null) return null;
nanoseconds += BigInt(matched[1] ?? "0") * (units[matched[2] ?? ""] ?? 0n);
offset = segment.lastIndex;
count += 1;
}
if (count === 0) return null;
if (matchSign[1] === "-") nanoseconds = -nanoseconds;
return `tekton-duration-ns:${nanoseconds}`;
}
export function canonicalSha256(value: unknown): string {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}
@@ -1027,6 +1067,10 @@ function firstCanonicalDrift(expected: unknown, actual: unknown, path: string):
function isProvenTektonAdmissionDefault(path: readonly (string | number)[], value: unknown): boolean {
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
const emptyRecord = isRecord(value) && Object.keys(value).length === 0;
if ([
"spec.workspaces.*.volumeClaimTemplate.metadata",
"spec.workspaces.*.volumeClaimTemplate.status",
].includes(normalized)) return emptyRecord;
const atTaskSpec = (suffix: string): boolean => [
`tasks.*.taskSpec.${suffix}`,
`spec.tasks.*.taskSpec.${suffix}`,
+26 -19
View File
@@ -13,6 +13,7 @@ const y = createYamlFieldReader(sub2RankConfigLabel);
export interface Sub2RankConfig {
version: number;
kind: "platform-infra-sub2rank";
warnings: string[];
metadata: { id: string; owner: string; relatedIssues: number[] };
defaults: { targetId: string };
application: {
@@ -30,6 +31,7 @@ export interface Sub2RankConfig {
sourceClean: boolean;
sourceRemotePresent: boolean;
sourceRemoteMatches: boolean;
sourceConfigPathMatches: boolean;
configMatchesSourceCommit: boolean;
deploymentBlockPresent: boolean;
automaticCreditEnabled: boolean;
@@ -67,7 +69,7 @@ export interface Sub2RankConfig {
project: string;
path: string;
destinationNamespace: string;
automated: true;
automated: boolean;
};
};
};
@@ -133,10 +135,11 @@ export function readSub2RankConfig(): Sub2RankConfig {
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, delivery, runtimeBase, secret, targets);
const warnings = validateResolvedConfig(application, image, delivery, runtimeBase, secret, targets);
return {
version,
kind: "platform-infra-sub2rank",
warnings,
metadata: {
id: y.stringField(metadataRecord, "id", "metadata"),
owner: y.stringField(metadataRecord, "owner", "metadata"),
@@ -192,7 +195,6 @@ function parseApplication(record: Record<string, unknown>): Sub2RankConfig["appl
const statusShort = gitText(sourceRoot, ["status", "--porcelain"], "read Sub2Rank source status", 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,
@@ -209,6 +211,7 @@ function parseApplication(record: Record<string, unknown>): Sub2RankConfig["appl
sourceClean: statusShort.length === 0,
sourceRemotePresent: observedRemote.length > 0,
sourceRemoteMatches: sameRepositoryIdentity(remote, observedRemote),
sourceConfigPathMatches: configRelativePath === sourceConfigPath,
configMatchesSourceCommit: committedConfig === configText.trimEnd(),
deploymentBlockPresent: app.deployment !== undefined,
automaticCreditEnabled,
@@ -241,7 +244,6 @@ function parseDelivery(record: Record<string, unknown>): Sub2RankConfig["deliver
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: {
@@ -278,7 +280,7 @@ function parseDelivery(record: Record<string, unknown>): Sub2RankConfig["deliver
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,
automated,
},
},
};
@@ -444,27 +446,32 @@ function validateResolvedConfig(
runtime: ReturnType<typeof parseRuntime>,
secret: ResolvedSecretDeclaration,
targets: Sub2RankTarget[],
): void {
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`);
): string[] {
const warnings: string[] = [];
if (!application.sourceConfigPathMatches) warnings.push("application.sourceConfigPath 与 application.configRef 的相对路径不一致");
if (!application.configMatchesSourceCommit) warnings.push(`application.configRef ${application.sourceCommit} `);
if (!application.sourceRemoteMatches) warnings.push("application.remote 与 application.sourceRoot 的 origin 不一致");
if (!sameRepositoryIdentity(application.remote, `https://github.com/${application.repository}.git`)) warnings.push("application.repository 与 application.remote 指向的仓库不一致");
if (!delivery.enabled) warnings.push("delivery.enabled=false,自动交付当前关闭");
if (delivery.gitops.maxPushAttempts > 5) warnings.push("delivery.gitops.maxPushAttempts 大于 5,失败时可能延长流水线耗时");
if (dirname(delivery.gitops.manifestPath).replaceAll("\\", "/") !== delivery.gitops.application.path) warnings.push("delivery.gitops.manifestPath 不在 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`);
if (!image.repository.startsWith("127.0.0.1:5000/")) warnings.push("image.repository 未使用 NC01 本地 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));
if (missing.length > 0) throw new Error(`${sub2RankConfigLabel}.runtime.secret.requiredTargetKeys missing from ${runtime.secret.configRef} declaration ${runtime.secret.declarationId}: ${missing.join(", ")}`);
if (!sameStringSet(application.server.envKeys, runtime.secret.appEnvTargetKeys)) throw new Error(`${sub2RankConfigLabel}.runtime.secret.appEnvTargetKeys must match application runtime server env keys`);
if (application.server.listenPort !== runtime.service.port) throw new Error(`${sub2RankConfigLabel}.runtime.service.port must match application runtime server listenPort`);
if (!application.server.databasePath.startsWith(`${runtime.storage.mountPath.replace(/\/+$/u, "")}/`)) throw new Error(`${sub2RankConfigLabel}.runtime.storage.mountPath must contain the application databasePath`);
if (!sameStringSet(application.server.envKeys, runtime.secret.appEnvTargetKeys)) warnings.push("runtime.secret.appEnvTargetKeys 与应用声明的环境变量集合不一致");
if (application.server.listenPort !== runtime.service.port) warnings.push("runtime.service.port 与应用 listenPort 不一致");
if (!application.server.databasePath.startsWith(`${runtime.storage.mountPath.replace(/\/+$/u, "")}/`)) warnings.push(" databasePath runtime.storage.mountPath ");
if (!delivery.gitops.application.automated) warnings.push("delivery.gitops.application.automated=falseArgo 自动同步当前关闭");
if (application.deploymentBlockPresent) warnings.push("应用配置仍包含 deployment 段;部署事实应以 platform-infra YAML 为准");
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`);
if (target.route !== secret.route || target.namespace !== secret.namespace) warnings.push(`targets[${target.id}] route/namespace Secret 目标 ${secret.targetId} 不一致`);
if (target.publicExposure.frpc.localPort !== runtime.service.port) warnings.push(`targets[${target.id}].publicExposure.frpc.localPort runtime.service.port 不一致`);
if (target.namespace !== delivery.gitops.application.destinationNamespace) warnings.push(`targets[${target.id}].namespace 与 Argo destinationNamespace 不一致`);
}
return warnings;
}
function positiveInteger(record: Record<string, unknown>, key: string, path: string): number {
@@ -34,7 +34,6 @@ export function renderSub2RankDesiredPipeline(binding: PacSourceArtifactBinding)
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,
@@ -95,8 +94,8 @@ function renderPipeline(
apiVersion: "tekton.dev/v1",
kind: "Pipeline",
metadata: {
name: delivery.pipeline.name,
namespace: delivery.pipeline.namespace,
name: binding.consumer.pipeline,
namespace: binding.consumer.namespace,
labels: {
"app.kubernetes.io/name": "sub2rank",
"app.kubernetes.io/part-of": "platform-infra",
@@ -208,57 +207,6 @@ function proxyEnv(): Record<string, unknown>[] {
];
}
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,
pipeline_timeout: delivery.pipeline.timeout,
workspace_pvc_size: delivery.pipeline.workspaceSize,
};
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}`);
}
if (binding.consumer.deliveryProvenance?.executionServiceAccountName !== delivery.pipeline.serviceAccountName) {
throw new Error(`Sub2Rank PaC deliveryProvenance execution ServiceAccount does not match owning YAML ${delivery.pipeline.serviceAccountName}`);
}
const application = delivery.gitops.application;
if (binding.consumer.argoNamespace !== application.namespace) throw new Error(`Sub2Rank PaC argoNamespace does not match owning YAML ${application.namespace}`);
if (binding.consumer.argoApplication !== application.name) throw new Error(`Sub2Rank PaC argoApplication does not match owning YAML ${application.name}`);
const bootstrap = binding.consumer.argoBootstrap;
if (bootstrap === null) throw new Error("Sub2Rank PaC argoBootstrap must be declared");
const expectedBootstrap = {
project: application.project,
repoUrl: delivery.gitops.readUrl,
targetRevision: delivery.gitops.branch,
path: application.path,
destinationNamespace: application.destinationNamespace,
automated: application.automated,
} as const;
for (const [key, value] of Object.entries(expectedBootstrap)) {
if (bootstrap[key as keyof typeof expectedBootstrap] !== value) throw new Error(`Sub2Rank PaC argoBootstrap.${key} does not match owning YAML ${String(value)}`);
}
}
function pipelineOwner(config: Sub2RankConfig, target: Sub2RankTarget): Record<string, unknown> {
return {
application: {
+3 -5
View File
@@ -58,6 +58,7 @@ function plan(options: OpsCommonOptions): Record<string, unknown> {
ok: policy.every((item) => item.ok),
action: "platform-infra-sub2rank-plan",
mutation: false,
warnings: sub2rank.warnings,
config: configSummary(sub2rank, target),
policy,
artifact: {
@@ -306,11 +307,6 @@ function policyChecks(config: Sub2RankConfig, target: Sub2RankTarget, yaml: stri
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)),
@@ -347,6 +343,7 @@ function configSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<s
function configRefsSummary(config: Sub2RankConfig): Record<string, unknown> {
return {
warnings: config.warnings,
deployment: { path: sub2RankConfigLabel, kind: config.kind },
application: {
path: config.application.configRef,
@@ -356,6 +353,7 @@ function configRefsSummary(config: Sub2RankConfig): Record<string, unknown> {
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 },