@intentius/chant-lexicon-k8s 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes YAML serializer.
|
|
3
|
+
*
|
|
4
|
+
* Converts Chant declarables to multi-document K8s YAML output with
|
|
5
|
+
* apiVersion, kind, metadata, and spec structure.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
9
|
+
import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
|
|
10
|
+
import { mergeResourceAttributes, RELEASE_ATTRIBUTES_VARIABLE, telemetryEnvironment } from "@intentius/chant/telemetry-attribution";
|
|
11
|
+
import { podSpecOf, releaseAttributesEnv } from "./telemetry-release.js";
|
|
12
|
+
import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
13
|
+
import { isEffectReceipt, receiptExpectation, referenceInputPaths, } from "@intentius/chant/effect-receipt";
|
|
14
|
+
import { RECEIPT_CONFIGMAP_REF, RECEIPT_DATA_KEY, RECEIPT_UNRESOLVED_VALUE_NOTE, receiptConfigMapRef, receiptNamespaceFrom, renderReceiptComment, } from "./effect-receipt-row.js";
|
|
15
|
+
import { walkValue } from "@intentius/chant/serializer-walker";
|
|
16
|
+
import { emitYAML } from "@intentius/chant/yaml";
|
|
17
|
+
import { isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
|
|
18
|
+
import { isRenderedManifestEntity } from "./manifest-entity.js";
|
|
19
|
+
import { projectKindForType } from "./project-kinds.js";
|
|
20
|
+
import { isEncryptedSecretFileEntity } from "./sops/entity.js";
|
|
21
|
+
import { isK8sInclude } from "./include/entity.js";
|
|
22
|
+
import { isRuleGroup, ruleGroupConfig } from "@intentius/chant-lexicon-prometheus";
|
|
23
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
24
|
+
/**
|
|
25
|
+
* K8s resource kinds whose properties live directly on the manifest
|
|
26
|
+
* (not nested under `spec`). These types use `data`, `stringData`,
|
|
27
|
+
* or have no spec field at all.
|
|
28
|
+
*/
|
|
29
|
+
const SPECLESS_TYPES = new Set([
|
|
30
|
+
"ConfigMap",
|
|
31
|
+
"Secret",
|
|
32
|
+
"Namespace",
|
|
33
|
+
"ServiceAccount",
|
|
34
|
+
"ClusterRole",
|
|
35
|
+
"ClusterRoleBinding",
|
|
36
|
+
"Role",
|
|
37
|
+
"RoleBinding",
|
|
38
|
+
"StorageClass",
|
|
39
|
+
"PersistentVolume",
|
|
40
|
+
"APIService",
|
|
41
|
+
]);
|
|
42
|
+
let cachedGVKMap = null;
|
|
43
|
+
function getGVKMap() {
|
|
44
|
+
if (cachedGVKMap)
|
|
45
|
+
return cachedGVKMap;
|
|
46
|
+
try {
|
|
47
|
+
cachedGVKMap = require("./generated/lexicon-k8s.json");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
cachedGVKMap = {};
|
|
51
|
+
}
|
|
52
|
+
return cachedGVKMap;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve entityType to apiVersion and kind.
|
|
56
|
+
*/
|
|
57
|
+
function resolveGVK(entityType) {
|
|
58
|
+
// A kind the project generated from its own CRDs (`chant generate`) wins
|
|
59
|
+
// over the packaged map, which knows nothing of it or an older version.
|
|
60
|
+
const projectKind = projectKindForType(entityType);
|
|
61
|
+
if (projectKind)
|
|
62
|
+
return { apiVersion: projectKind.apiVersion, kind: projectKind.kind };
|
|
63
|
+
const gvkMap = getGVKMap();
|
|
64
|
+
// Search for matching entry by resourceType
|
|
65
|
+
for (const entry of Object.values(gvkMap)) {
|
|
66
|
+
if (entry.resourceType === entityType && entry.apiVersion && entry.gvkKind) {
|
|
67
|
+
return { apiVersion: entry.apiVersion, kind: entry.gvkKind };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Fallback: derive from entity type string (K8s::Group::Kind → group/v1, Kind)
|
|
71
|
+
return deriveGVKFromType(entityType);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Well-known K8s API group → apiVersion mappings for fallback when
|
|
75
|
+
* the generated lexicon JSON is not available.
|
|
76
|
+
*/
|
|
77
|
+
const API_GROUP_VERSIONS = {
|
|
78
|
+
Core: "v1",
|
|
79
|
+
Apps: "apps/v1",
|
|
80
|
+
Batch: "batch/v1",
|
|
81
|
+
Networking: "networking.k8s.io/v1",
|
|
82
|
+
Policy: "policy/v1",
|
|
83
|
+
Rbac: "rbac.authorization.k8s.io/v1",
|
|
84
|
+
Storage: "storage.k8s.io/v1",
|
|
85
|
+
Autoscaling: "autoscaling/v2",
|
|
86
|
+
Admissionregistration: "admissionregistration.k8s.io/v1",
|
|
87
|
+
GKE: "cloud.google.com/v1",
|
|
88
|
+
NetworkingGKE: "networking.gke.io/v1",
|
|
89
|
+
NetworkingGKEBeta: "networking.gke.io/v1beta1",
|
|
90
|
+
// Common Kubernetes operator CRDs
|
|
91
|
+
CertManager: "cert-manager.io/v1",
|
|
92
|
+
ExternalSecrets: "external-secrets.io/v1",
|
|
93
|
+
Monitoring: "monitoring.coreos.com/v1",
|
|
94
|
+
// KubeRay operator CRDs
|
|
95
|
+
Ray: "ray.io/v1",
|
|
96
|
+
};
|
|
97
|
+
function deriveGVKFromType(entityType) {
|
|
98
|
+
// Format: K8s::Group::Kind
|
|
99
|
+
const parts = entityType.split("::");
|
|
100
|
+
if (parts.length !== 3 || parts[0] !== "K8s")
|
|
101
|
+
return null;
|
|
102
|
+
const group = parts[1];
|
|
103
|
+
const kind = parts[2];
|
|
104
|
+
const apiVersion = API_GROUP_VERSIONS[group];
|
|
105
|
+
if (!apiVersion)
|
|
106
|
+
return null;
|
|
107
|
+
return { apiVersion, kind };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The name a resource gets when it does not declare one: the logical name,
|
|
111
|
+
* kebab-cased. Shared with the manifest builder below so a reference and the
|
|
112
|
+
* thing it references cannot derive different names (#1493).
|
|
113
|
+
*/
|
|
114
|
+
function derivedName(logicalName) {
|
|
115
|
+
return logicalName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What a cross-resource reference resolves to on this substrate (#1493).
|
|
119
|
+
*
|
|
120
|
+
* Kubernetes YAML has no deploy-time reference mechanism — no `!Ref`, no
|
|
121
|
+
* `Fn::GetAtt`. Whatever a reference is going to mean has to be resolved here,
|
|
122
|
+
* before serialization, or it means nothing at all.
|
|
123
|
+
*
|
|
124
|
+
* This used to return the *logical* name for every attribute, so
|
|
125
|
+
* `claimName: pgClaim.name` emitted `claimName: pgClaim` and the manifest
|
|
126
|
+
* named a resource that does not exist. It applied cleanly and failed on the
|
|
127
|
+
* cluster, which is the failure this whole system exists to remove. The
|
|
128
|
+
* lexicon's own plugin comment already described the intended behaviour —
|
|
129
|
+
* "resolves to metadata.name" — so this brings the code to the documentation
|
|
130
|
+
* rather than the other way round.
|
|
131
|
+
*
|
|
132
|
+
* `uid` is refused rather than resolved: a UID is assigned by the API server
|
|
133
|
+
* at admission, so no build-time value exists and any string emitted here
|
|
134
|
+
* would be a fabrication.
|
|
135
|
+
*/
|
|
136
|
+
function resolveK8sAttr(entity, logicalName, attr) {
|
|
137
|
+
if (attr === "uid") {
|
|
138
|
+
throw new Error(`Cannot reference "${logicalName}.uid": a Kubernetes UID is assigned by the API server at ` +
|
|
139
|
+
`admission, so it has no build-time value. Reference .name, or carry the UID at runtime ` +
|
|
140
|
+
`with fieldRef: { fieldPath: "metadata.uid" }.`);
|
|
141
|
+
}
|
|
142
|
+
const metadata = entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
|
|
143
|
+
? entity.props.metadata
|
|
144
|
+
: undefined;
|
|
145
|
+
if (attr === "name") {
|
|
146
|
+
// Same fallback the manifest builder applies, so a reference to a resource
|
|
147
|
+
// that declares no name still resolves to the name it will actually get.
|
|
148
|
+
const declared = metadata?.name;
|
|
149
|
+
return typeof declared === "string" && declared.length > 0 ? declared : derivedName(logicalName);
|
|
150
|
+
}
|
|
151
|
+
if (attr === "namespace") {
|
|
152
|
+
const ns = metadata?.namespace;
|
|
153
|
+
if (typeof ns === "string" && ns.length > 0)
|
|
154
|
+
return ns;
|
|
155
|
+
throw new Error(`Cannot reference "${logicalName}.namespace": it declares no metadata.namespace, and the ` +
|
|
156
|
+
`namespace a manifest lands in is decided at apply time (kubectl -n, or the context's ` +
|
|
157
|
+
`default). Set metadata.namespace on "${logicalName}" if the reference needs to be stable.`);
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Cannot reference "${logicalName}.${attr}": the k8s lexicon resolves .name and .namespace at ` +
|
|
160
|
+
`build time, and Kubernetes YAML has no way to express any other attribute reference.`);
|
|
161
|
+
}
|
|
162
|
+
/** The rendered expectation: the synthesis-time value when the receipt is
|
|
163
|
+
* fully static, the placeholder note when reference inputs remain (#1703
|
|
164
|
+
* decision 5, since synthesis resolves nothing). */
|
|
165
|
+
function receiptRowValue(receipt) {
|
|
166
|
+
if (receipt.flavor === "hash" && referenceInputPaths(receipt).length > 0) {
|
|
167
|
+
return RECEIPT_UNRESOLVED_VALUE_NOTE;
|
|
168
|
+
}
|
|
169
|
+
return receiptExpectation(receipt);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Render the effect receipts (#2074) the build withheld from the apply-bound
|
|
173
|
+
* entity set (`SerializeContext.receipts`, #1832) as ConfigMap rows: named
|
|
174
|
+
* `chant-receipt.<stack>.<env>.<effect>` from the ownership marker fields
|
|
175
|
+
* (epic decision 4), in the project's receipt namespace, with the expectation
|
|
176
|
+
* under `data.expectation`.
|
|
177
|
+
*
|
|
178
|
+
* Visibility only. The rows ride a YAML comment at the end of the manifest
|
|
179
|
+
* stream (./effect-receipt-row.ts) rather than a document, because a document
|
|
180
|
+
* is what an applier applies and the `effect()` step is a receipt's sole
|
|
181
|
+
* writer (#1832, epic #1703 decision 3).
|
|
182
|
+
*
|
|
183
|
+
* The env segment is explicit: a receipt with no resolved `ownership.env` is
|
|
184
|
+
* an error, never a guessed segment.
|
|
185
|
+
*/
|
|
186
|
+
function renderReceiptRows(receipts, ownership, namespace) {
|
|
187
|
+
const rows = {};
|
|
188
|
+
const names = [...receipts.keys()].join(", ");
|
|
189
|
+
if (!ownership?.stack) {
|
|
190
|
+
throw new Error(`k8s receipts (${names}): no ownership marker resolved. The receipt ConfigMap is named ` +
|
|
191
|
+
`chant-receipt.<stack>.<env>.<effect>, derived from the same ownership fields that ` +
|
|
192
|
+
`stamp markers (chant #1703, decision 4). Set ownership: { stack } in chant.config.ts.`);
|
|
193
|
+
}
|
|
194
|
+
if (!ownership.env) {
|
|
195
|
+
throw new Error(`k8s receipts (${names}): ownership resolved no env. The receipt name's <env> segment is ` +
|
|
196
|
+
`explicit (chant #1703, decision 4). Set ownership: { env } in chant.config.ts, or ` +
|
|
197
|
+
`build with an env-valued parameter that resolves it.`);
|
|
198
|
+
}
|
|
199
|
+
for (const [name, entity] of receipts) {
|
|
200
|
+
if (!isEffectReceipt(entity))
|
|
201
|
+
continue;
|
|
202
|
+
const ref = receiptConfigMapRef(ownership.stack, ownership.env, entity.effect, namespace);
|
|
203
|
+
rows[name] = {
|
|
204
|
+
kind: RECEIPT_CONFIGMAP_REF.kind,
|
|
205
|
+
namespace: ref.namespace,
|
|
206
|
+
name: ref.name,
|
|
207
|
+
data: { [RECEIPT_DATA_KEY]: receiptRowValue(entity) },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return rows;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* K8s visitor for the generic serializer walker.
|
|
214
|
+
*/
|
|
215
|
+
function k8sVisitor(entityNames) {
|
|
216
|
+
// name → entity, so an AttrRef can reach the resource it points at. The
|
|
217
|
+
// walker hands the visitor a logical name, not the Declarable.
|
|
218
|
+
const byName = new Map();
|
|
219
|
+
for (const [entity, name] of entityNames)
|
|
220
|
+
byName.set(name, entity);
|
|
221
|
+
return {
|
|
222
|
+
attrRef: (name, attr) => resolveK8sAttr(byName.get(name), name, attr),
|
|
223
|
+
// A prometheus-lexicon RuleGroup (in a PrometheusRule's spec.groups) is
|
|
224
|
+
// data, not a reference: it renders as the same group the rule file
|
|
225
|
+
// holds, whether it was exported as its own entity or written inline.
|
|
226
|
+
resourceRef: (name) => {
|
|
227
|
+
const entity = byName.get(name);
|
|
228
|
+
return isRuleGroup(entity) ? ruleGroupConfig(entity) : name;
|
|
229
|
+
},
|
|
230
|
+
propertyDeclarable: (entity, walk) => {
|
|
231
|
+
if (isRuleGroup(entity))
|
|
232
|
+
return ruleGroupConfig(entity);
|
|
233
|
+
if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
const props = entity.props;
|
|
237
|
+
const result = {};
|
|
238
|
+
for (const [key, value] of Object.entries(props)) {
|
|
239
|
+
if (value !== undefined) {
|
|
240
|
+
result[key] = walk(value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Convert a value to YAML-compatible form using the walker.
|
|
249
|
+
*/
|
|
250
|
+
function toYAMLValue(value, entityNames) {
|
|
251
|
+
return walkValue(value, entityNames, k8sVisitor(entityNames));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Kubernetes YAML serializer implementation.
|
|
255
|
+
*/
|
|
256
|
+
// ── Telemetry attribution (#2558, D22, #3059) ──────────────────────
|
|
257
|
+
/** The digest of an image reference pinned with `@sha256:...`, or undefined. */
|
|
258
|
+
function imageDigest(image) {
|
|
259
|
+
if (typeof image !== "string")
|
|
260
|
+
return undefined;
|
|
261
|
+
const at = image.lastIndexOf("@");
|
|
262
|
+
return at === -1 ? undefined : image.slice(at + 1);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Stamp `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` into the `env` of
|
|
266
|
+
* each container of a workload. `service.name` is the workload's
|
|
267
|
+
* `metadata.name`, `chant.decl` its declaration id (`decl`), and an image
|
|
268
|
+
* pinned by digest gives `service.version`. A variable the container already
|
|
269
|
+
* sets is kept: its own `OTEL_SERVICE_NAME` wins, and its own resource
|
|
270
|
+
* attributes keep their keys while the missing ones are appended. One set
|
|
271
|
+
* through `valueFrom` is left alone. Init containers are not stamped.
|
|
272
|
+
*
|
|
273
|
+
* `OTEL_RESOURCE_ATTRIBUTES` ends with `$(CHANT_RELEASE_ATTRIBUTES)`, read
|
|
274
|
+
* from the pod annotation a release deploy sets (#3061, ws-081), so a
|
|
275
|
+
* release adds `service.version` and `vcs.ref.head.revision` without
|
|
276
|
+
* changing this output.
|
|
277
|
+
*/
|
|
278
|
+
function stampWorkloadTelemetry(manifest, decl, telemetry) {
|
|
279
|
+
const podSpec = podSpecOf(manifest);
|
|
280
|
+
const containers = podSpec?.containers;
|
|
281
|
+
if (!Array.isArray(containers))
|
|
282
|
+
return;
|
|
283
|
+
const service = String(manifest.metadata?.name ?? decl);
|
|
284
|
+
const releaseRef = `$(${RELEASE_ATTRIBUTES_VARIABLE})`;
|
|
285
|
+
for (const container of containers) {
|
|
286
|
+
if (!container || typeof container !== "object")
|
|
287
|
+
continue;
|
|
288
|
+
const c = container;
|
|
289
|
+
const version = imageDigest(c.image);
|
|
290
|
+
const vars = telemetryEnvironment(telemetry, { service, decl, ...(version ? { version } : {}) });
|
|
291
|
+
if (c.env !== undefined && !Array.isArray(c.env) && typeof c.env === "object" && c.env !== null) {
|
|
292
|
+
// A map, as some composites and hand-written props give it before the API's list form. A map
|
|
293
|
+
// has no order, so it cannot hold the $(VAR) reference to the release attributes.
|
|
294
|
+
const map = { ...c.env };
|
|
295
|
+
if (!("OTEL_SERVICE_NAME" in map))
|
|
296
|
+
map.OTEL_SERVICE_NAME = vars.OTEL_SERVICE_NAME;
|
|
297
|
+
const own = map.OTEL_RESOURCE_ATTRIBUTES;
|
|
298
|
+
if (own === undefined)
|
|
299
|
+
map.OTEL_RESOURCE_ATTRIBUTES = vars.OTEL_RESOURCE_ATTRIBUTES;
|
|
300
|
+
else if (typeof own === "string")
|
|
301
|
+
map.OTEL_RESOURCE_ATTRIBUTES = mergeResourceAttributes(own, vars.OTEL_RESOURCE_ATTRIBUTES);
|
|
302
|
+
c.env = map;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const env = Array.isArray(c.env) ? [...c.env] : [];
|
|
306
|
+
const at = (key) => env.findIndex((e) => e && typeof e === "object" && e.name === key);
|
|
307
|
+
if (at("OTEL_SERVICE_NAME") === -1)
|
|
308
|
+
env.push({ name: "OTEL_SERVICE_NAME", value: vars.OTEL_SERVICE_NAME });
|
|
309
|
+
let i = at("OTEL_RESOURCE_ATTRIBUTES");
|
|
310
|
+
if (i === -1) {
|
|
311
|
+
env.push({ name: "OTEL_RESOURCE_ATTRIBUTES", value: vars.OTEL_RESOURCE_ATTRIBUTES });
|
|
312
|
+
i = env.length - 1;
|
|
313
|
+
}
|
|
314
|
+
else if (typeof env[i].value === "string") {
|
|
315
|
+
env[i] = { ...env[i], value: mergeResourceAttributes(env[i].value, vars.OTEL_RESOURCE_ATTRIBUTES) };
|
|
316
|
+
}
|
|
317
|
+
// The release attributes (#3061, ws-081): a $(VAR) reference expands only to a variable defined
|
|
318
|
+
// earlier in the list, so the downward-API entry goes just before OTEL_RESOURCE_ATTRIBUTES. A
|
|
319
|
+
// container that defines the variable itself keeps its own, and gets the reference only when
|
|
320
|
+
// its definition comes first.
|
|
321
|
+
if (typeof env[i].value === "string") {
|
|
322
|
+
if (at(RELEASE_ATTRIBUTES_VARIABLE) === -1) {
|
|
323
|
+
env.splice(i, 0, releaseAttributesEnv());
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
const value = env[i].value;
|
|
327
|
+
if (at(RELEASE_ATTRIBUTES_VARIABLE) < i && !value.includes(releaseRef))
|
|
328
|
+
env[i] = { ...env[i], value: `${value}${releaseRef}` };
|
|
329
|
+
}
|
|
330
|
+
c.env = env;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export const k8sSerializer = {
|
|
334
|
+
name: "k8s",
|
|
335
|
+
rulePrefix: "WK8",
|
|
336
|
+
// The Argo CD and Flux checks are distinct product surfaces this lexicon
|
|
337
|
+
// covers, and their ids are published — renaming them would break every
|
|
338
|
+
// `chant-disable ARGO001` already written (#1349).
|
|
339
|
+
extraRulePrefixes: ["ARGO", "FLUX"],
|
|
340
|
+
serialize(entities, _outputs, context) {
|
|
341
|
+
// Build reverse map: entity → name
|
|
342
|
+
const entityNames = new Map();
|
|
343
|
+
for (const [name, entity] of entities) {
|
|
344
|
+
entityNames.set(entity, name);
|
|
345
|
+
}
|
|
346
|
+
// Collect default labels and annotations. Ownership markers are stamped as
|
|
347
|
+
// labels, so they seed defaultLabelEntries and flow through the same merge
|
|
348
|
+
// (explicit resource labels still win).
|
|
349
|
+
let defaultLabelEntries = context?.ownership
|
|
350
|
+
? { ...ownershipEntries(LABEL_OWNERSHIP_KEYS, context.ownership) }
|
|
351
|
+
: {};
|
|
352
|
+
let defaultAnnotationEntries = {};
|
|
353
|
+
for (const [, entity] of entities) {
|
|
354
|
+
if (isDefaultLabels(entity)) {
|
|
355
|
+
defaultLabelEntries = { ...defaultLabelEntries, ...entity.labels };
|
|
356
|
+
}
|
|
357
|
+
if (isDefaultAnnotations(entity)) {
|
|
358
|
+
defaultAnnotationEntries = { ...defaultAnnotationEntries, ...entity.annotations };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const namespaceDocs = [];
|
|
362
|
+
const otherDocs = [];
|
|
363
|
+
/** Sidecar files — committed ciphertext, copied byte-for-byte. */
|
|
364
|
+
const files = {};
|
|
365
|
+
/** Basenames within `files` that must never be JSON.parse round-tripped (chant#1937). */
|
|
366
|
+
const verbatimFiles = [];
|
|
367
|
+
const warnings = [];
|
|
368
|
+
for (const [name, entity] of entities) {
|
|
369
|
+
if (isPropertyDeclarable(entity))
|
|
370
|
+
continue;
|
|
371
|
+
if (isDefaultLabels(entity) || isDefaultAnnotations(entity))
|
|
372
|
+
continue;
|
|
373
|
+
// A `k8sInclude` declaration is resolved into its documents at
|
|
374
|
+
// `buildRoots()`; the declaration itself emits nothing.
|
|
375
|
+
if (isK8sInclude(entity))
|
|
376
|
+
continue;
|
|
377
|
+
// Committed SOPS ciphertext (epic lex00/iac-cd-bench#6) leaves as a
|
|
378
|
+
// SIDECAR and never as a document in the primary output. That is the
|
|
379
|
+
// load-bearing line: chant's appliers read the primary output, so an
|
|
380
|
+
// undecrypted Secret cannot reach a cluster through them. The bytes are
|
|
381
|
+
// copied exactly as committed — no re-emit, no label merge, no
|
|
382
|
+
// ownership stamp — because re-serializing would break the MAC the
|
|
383
|
+
// `sops` block carries over the plaintext.
|
|
384
|
+
if (isEncryptedSecretFileEntity(entity)) {
|
|
385
|
+
const existing = files[entity.filename];
|
|
386
|
+
if (existing !== undefined && existing !== entity.text) {
|
|
387
|
+
warnings.push(`k8s: two committed-encrypted secrets emit the same sidecar filename ` +
|
|
388
|
+
`"${entity.filename}" — the second (${entity.sourcePath}) wins`);
|
|
389
|
+
}
|
|
390
|
+
files[entity.filename] = entity.text;
|
|
391
|
+
if (!verbatimFiles.includes(entity.filename))
|
|
392
|
+
verbatimFiles.push(entity.filename);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// A kustomize build root's document (#1548 piece 3) is render-final:
|
|
396
|
+
// the overlay decided every field, so the props ARE the manifest and
|
|
397
|
+
// the spec-inference heuristics below (built for typed declarables)
|
|
398
|
+
// must not reshape it — a doc whose top-level fields aren't `spec`
|
|
399
|
+
// (webhooks, rules on a CRD instance) would otherwise be re-nested.
|
|
400
|
+
// It still gets the exact default-label/annotation merge every
|
|
401
|
+
// discovered resource gets, which is what stamps ownership on it.
|
|
402
|
+
if (isRenderedManifestEntity(entity)) {
|
|
403
|
+
const manifest = { ...entity.props };
|
|
404
|
+
const metadata = { ...(manifest.metadata ?? {}) };
|
|
405
|
+
if (Object.keys(defaultLabelEntries).length > 0) {
|
|
406
|
+
metadata.labels = { ...defaultLabelEntries, ...(metadata.labels ?? {}) };
|
|
407
|
+
}
|
|
408
|
+
if (Object.keys(defaultAnnotationEntries).length > 0) {
|
|
409
|
+
metadata.annotations = {
|
|
410
|
+
...defaultAnnotationEntries,
|
|
411
|
+
...(metadata.annotations ?? {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
manifest.metadata = metadata;
|
|
415
|
+
const yamlDoc = emitK8sManifest(manifest);
|
|
416
|
+
if (manifest.kind === "Namespace") {
|
|
417
|
+
namespaceDocs.push(yamlDoc);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
otherDocs.push(yamlDoc);
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const entityType = entity.entityType;
|
|
425
|
+
const gvk = resolveGVK(entityType);
|
|
426
|
+
if (!gvk)
|
|
427
|
+
continue;
|
|
428
|
+
const props = toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames);
|
|
429
|
+
if (!props)
|
|
430
|
+
continue;
|
|
431
|
+
// Build the K8s manifest structure
|
|
432
|
+
const manifest = {
|
|
433
|
+
apiVersion: gvk.apiVersion,
|
|
434
|
+
kind: gvk.kind,
|
|
435
|
+
};
|
|
436
|
+
// Build metadata
|
|
437
|
+
const metadata = props.metadata ?? {};
|
|
438
|
+
if (!metadata.name) {
|
|
439
|
+
metadata.name = derivedName(name);
|
|
440
|
+
}
|
|
441
|
+
// Merge default labels
|
|
442
|
+
if (Object.keys(defaultLabelEntries).length > 0) {
|
|
443
|
+
const existingLabels = (metadata.labels ?? {});
|
|
444
|
+
metadata.labels = { ...defaultLabelEntries, ...existingLabels };
|
|
445
|
+
}
|
|
446
|
+
// Merge default annotations
|
|
447
|
+
if (Object.keys(defaultAnnotationEntries).length > 0) {
|
|
448
|
+
const existingAnnotations = (metadata.annotations ?? {});
|
|
449
|
+
metadata.annotations = { ...defaultAnnotationEntries, ...existingAnnotations };
|
|
450
|
+
}
|
|
451
|
+
manifest.metadata = metadata;
|
|
452
|
+
// Properties that always belong at the manifest root, never inside
|
|
453
|
+
// spec. apiVersion/kind allow consumers to override the gvk-derived
|
|
454
|
+
// defaults (the "CRD wrapper" trick used to declare arbitrary K8s
|
|
455
|
+
// resources via a generic Declarable class). rules/subjects/roleRef
|
|
456
|
+
// are the top-level fields for RBAC kinds. data/stringData are for
|
|
457
|
+
// ConfigMap/Secret. binaryData covers ConfigMap binary entries.
|
|
458
|
+
const TOP_LEVEL_PROPS = new Set([
|
|
459
|
+
"apiVersion",
|
|
460
|
+
"kind",
|
|
461
|
+
"rules",
|
|
462
|
+
"subjects",
|
|
463
|
+
"roleRef",
|
|
464
|
+
"data",
|
|
465
|
+
"stringData",
|
|
466
|
+
"binaryData",
|
|
467
|
+
"type",
|
|
468
|
+
"immutable",
|
|
469
|
+
"automountServiceAccountToken",
|
|
470
|
+
"secrets",
|
|
471
|
+
"imagePullSecrets",
|
|
472
|
+
]);
|
|
473
|
+
// The remaining properties go under spec (or directly on the manifest for certain types)
|
|
474
|
+
if (SPECLESS_TYPES.has(gvk.kind)) {
|
|
475
|
+
// These types have their data directly on the manifest (data, stringData, etc.)
|
|
476
|
+
for (const [key, value] of Object.entries(props)) {
|
|
477
|
+
if (key !== "metadata") {
|
|
478
|
+
manifest[key] = value;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (props.spec !== undefined) {
|
|
483
|
+
// If spec is already set, use it directly
|
|
484
|
+
manifest.spec = props.spec;
|
|
485
|
+
for (const [key, value] of Object.entries(props)) {
|
|
486
|
+
if (key !== "metadata" && key !== "spec") {
|
|
487
|
+
manifest[key] = value;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// Place remaining props under spec — except known top-level fields
|
|
493
|
+
// (apiVersion/kind for CRD-wrapper overrides; rules/subjects for RBAC).
|
|
494
|
+
const spec = {};
|
|
495
|
+
for (const [key, value] of Object.entries(props)) {
|
|
496
|
+
if (key === "metadata")
|
|
497
|
+
continue;
|
|
498
|
+
if (TOP_LEVEL_PROPS.has(key)) {
|
|
499
|
+
manifest[key] = value;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
spec[key] = value;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (Object.keys(spec).length > 0) {
|
|
506
|
+
manifest.spec = spec;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (context?.telemetry)
|
|
510
|
+
stampWorkloadTelemetry(manifest, name, context.telemetry);
|
|
511
|
+
// Emit as YAML — sort Namespaces first so kubectl apply succeeds
|
|
512
|
+
const yamlDoc = emitK8sManifest(manifest);
|
|
513
|
+
if (gvk.kind === "Namespace") {
|
|
514
|
+
namespaceDocs.push(yamlDoc);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
otherDocs.push(yamlDoc);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
let primary = [...namespaceDocs, ...otherDocs].join("\n---\n");
|
|
521
|
+
// Effect receipt rows (#2074): visibility only, deliberately NOT a
|
|
522
|
+
// document: appliers apply documents, and the `effect()` step is a
|
|
523
|
+
// receipt's sole writer (#1832, epic #1703 decision 3). The comment rides
|
|
524
|
+
// the same build output the observation leg is handed, which is how it
|
|
525
|
+
// learns each receipt's ConfigMap address.
|
|
526
|
+
if (context?.receipts && context.receipts.size > 0) {
|
|
527
|
+
const rows = renderReceiptRows(context.receipts, context.ownership, receiptNamespaceFrom(context.config));
|
|
528
|
+
if (Object.keys(rows).length > 0) {
|
|
529
|
+
primary = primary.length > 0 ? `${primary}\n${renderReceiptComment(rows)}\n` : `${renderReceiptComment(rows)}\n`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// A bare string when there is nothing extra to write, so the common case
|
|
533
|
+
// stays byte-identical to what every existing consumer already reads.
|
|
534
|
+
if (Object.keys(files).length === 0 && warnings.length === 0)
|
|
535
|
+
return primary;
|
|
536
|
+
return {
|
|
537
|
+
primary,
|
|
538
|
+
files,
|
|
539
|
+
// Every verbatim file here is committed ciphertext, so each is also a
|
|
540
|
+
// secret: a build with no --output refuses rather than echo it (chant#3738).
|
|
541
|
+
...(verbatimFiles.length > 0 ? { verbatimFiles, secretFiles: [...verbatimFiles] } : {}),
|
|
542
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Emit a K8s manifest object as YAML.
|
|
548
|
+
* Preserves key ordering: apiVersion, kind, metadata, spec, then rest.
|
|
549
|
+
*/
|
|
550
|
+
/**
|
|
551
|
+
* Emit a key-value pair as YAML. Scalars get ` value` suffix; objects get
|
|
552
|
+
* block-style indented below the key.
|
|
553
|
+
*/
|
|
554
|
+
function emitKeyValue(key, value) {
|
|
555
|
+
const yamlStr = emitYAML(value, 1);
|
|
556
|
+
// If the YAML starts with a newline, it's a block value (object/array)
|
|
557
|
+
if (yamlStr.startsWith("\n")) {
|
|
558
|
+
return `${key}:${yamlStr}`;
|
|
559
|
+
}
|
|
560
|
+
return `${key}: ${yamlStr}`;
|
|
561
|
+
}
|
|
562
|
+
function emitK8sManifest(manifest) {
|
|
563
|
+
const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
|
|
564
|
+
const lines = [];
|
|
565
|
+
// Emit ordered keys first
|
|
566
|
+
for (const key of orderedKeys) {
|
|
567
|
+
if (manifest[key] !== undefined) {
|
|
568
|
+
lines.push(emitKeyValue(key, manifest[key]));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// Emit remaining keys
|
|
572
|
+
for (const [key, value] of Object.entries(manifest)) {
|
|
573
|
+
if (!orderedKeys.includes(key) && value !== undefined) {
|
|
574
|
+
lines.push(emitKeyValue(key, value));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return lines.join("\n") + "\n";
|
|
578
|
+
}
|