diff --git a/scripts/src/platform-infra-sub2rank-config.ts b/scripts/src/platform-infra-sub2rank-config.ts index aa1f6d7f..2897fc29 100644 --- a/scripts/src/platform-infra-sub2rank-config.ts +++ b/scripts/src/platform-infra-sub2rank-config.ts @@ -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): 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): 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): 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): 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, 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=false,Argo 自动同步当前关闭"); + 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, key: string, path: string): number { diff --git a/scripts/src/platform-infra-sub2rank-pipeline.ts b/scripts/src/platform-infra-sub2rank-pipeline.ts index 91b99f57..222e7124 100644 --- a/scripts/src/platform-infra-sub2rank-pipeline.ts +++ b/scripts/src/platform-infra-sub2rank-pipeline.ts @@ -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[] { ]; } -function assertBinding(config: Sub2RankConfig, target: Sub2RankTarget, binding: PacSourceArtifactBinding): void { - const delivery = config.delivery; - const expected: Readonly> = { - 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> = { - 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 { return { application: { diff --git a/scripts/src/platform-infra-sub2rank.ts b/scripts/src/platform-infra-sub2rank.ts index 691e20d6..a94a8196 100644 --- a/scripts/src/platform-infra-sub2rank.ts +++ b/scripts/src/platform-infra-sub2rank.ts @@ -58,6 +58,7 @@ function plan(options: OpsCommonOptions): Record { 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 { return { + warnings: config.warnings, deployment: { path: sub2RankConfigLabel, kind: config.kind }, application: { path: config.application.configRef, @@ -356,6 +353,7 @@ function configRefsSummary(config: Sub2RankConfig): Record { 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 },