@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API group → `K8s::{Namespace}::{Kind}` namespace segment — the ONE copy.
|
|
3
|
+
*
|
|
4
|
+
* Four surfaces build entityTypes from a group string: CRD codegen
|
|
5
|
+
* (`crd/parser.ts`), the swagger pass (`spec/parse.ts`), live discovery
|
|
6
|
+
* (`describe-resources.ts` via `gvkToTypeName`), and YAML import
|
|
7
|
+
* (`import/parser.ts`). Until #1628 each had its own copy of the rule and only
|
|
8
|
+
* CRD codegen consulted the overrides, so a Flux Kustomization arriving through
|
|
9
|
+
* a kustomize root or `chant import` was typed `K8s::Kustomize::Kustomization`
|
|
10
|
+
* — a name that matches nothing in operations.json (NOT-OBSERVED live) and
|
|
11
|
+
* nothing downstream that dispatches on kind. Every path resolves here now;
|
|
12
|
+
* the declared, rendered, imported and discovered spellings of a kind cannot
|
|
13
|
+
* skew.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Group names whose first segment doesn't yield the conventional namespace.
|
|
17
|
+
* "argoproj.io" → "Argo" (not "Argoproj") to match the Argo CD vocabulary
|
|
18
|
+
* and the ArgoAppFor / ArgoAppSetForRegions composites.
|
|
19
|
+
*
|
|
20
|
+
* The Flux toolkit spreads across five `*.toolkit.fluxcd.io` groups plus the
|
|
21
|
+
* Flux Operator's `fluxcd.controlplane.io`; all six collapse to a single `Flux`
|
|
22
|
+
* namespace so a GitRepository and a Kustomization read as `K8s::Flux::*`
|
|
23
|
+
* siblings rather than scattering to Source / Kustomize / Helm / Notification /
|
|
24
|
+
* Image / Fluxcd. (`helm.toolkit.fluxcd.io` → `Helm` would also collide
|
|
25
|
+
* confusingly with the separate helm lexicon.)
|
|
26
|
+
*
|
|
27
|
+
* KubeMicroVM's `lambda.aws.amazon.com` would take `Lambda` by the
|
|
28
|
+
* first-segment rule, which reads as AWS Lambda proper and would sit
|
|
29
|
+
* confusingly beside the aws lexicon's real Lambda functions. These are
|
|
30
|
+
* Kubernetes CRs belonging to a community operator, so they take the
|
|
31
|
+
* operator's own name. `MicroVM` alone was the alternative and was rejected
|
|
32
|
+
* for stuttering: `K8s::MicroVM::MicroVM`.
|
|
33
|
+
*
|
|
34
|
+
* Note the official AWS controller uses a different group,
|
|
35
|
+
* `lambdamicrovms.services.k8s.aws`, so it can coexist here and will want its
|
|
36
|
+
* own entry rather than sharing this one.
|
|
37
|
+
*/
|
|
38
|
+
export const GROUP_NAMESPACE_OVERRIDES = {
|
|
39
|
+
"argoproj.io": "Argo",
|
|
40
|
+
"source.toolkit.fluxcd.io": "Flux",
|
|
41
|
+
"kustomize.toolkit.fluxcd.io": "Flux",
|
|
42
|
+
"helm.toolkit.fluxcd.io": "Flux",
|
|
43
|
+
"notification.toolkit.fluxcd.io": "Flux",
|
|
44
|
+
"image.toolkit.fluxcd.io": "Flux",
|
|
45
|
+
"fluxcd.controlplane.io": "Flux",
|
|
46
|
+
"lambda.aws.amazon.com": "KubeMicroVM",
|
|
47
|
+
// CNPG and its barman-cloud plugin ship under two groups but are one thing to
|
|
48
|
+
// an author: a Cluster's `plugins[]` names an ObjectStore. The first-segment
|
|
49
|
+
// rule would scatter them into `Postgresql` and `Barmancloud`.
|
|
50
|
+
"postgresql.cnpg.io": "Cnpg",
|
|
51
|
+
"barmancloud.cnpg.io": "Cnpg",
|
|
52
|
+
// Not `Secrets`: `K8s::Secrets::InfisicalSecret` reads like a core Secret,
|
|
53
|
+
// and `K8s::Core::Secret` is right there to be confused with.
|
|
54
|
+
"secrets.infisical.com": "Infisical",
|
|
55
|
+
// k3s's bundled controllers ship under two groups but are one thing to an
|
|
56
|
+
// author — the k3s auto-deploy surface. The first-segment rule would give
|
|
57
|
+
// `K8s::Helm::HelmChart`, which reads like it belongs to the helm lexicon,
|
|
58
|
+
// and would split HelmChart from the Addon that tracks its deployment.
|
|
59
|
+
"helm.cattle.io": "K3s",
|
|
60
|
+
"k3s.cattle.io": "K3s",
|
|
61
|
+
// The first-segment rule would give `Serving`, which reads like a generic
|
|
62
|
+
// core concept rather than the KServe operator that owns it.
|
|
63
|
+
"serving.kserve.io": "KServe",
|
|
64
|
+
// The first-segment rule would give `Cluster`, which stutters against the
|
|
65
|
+
// CAPI `Cluster` kind itself (`K8s::Cluster::Cluster`) the same way the
|
|
66
|
+
// rejected `K8s::MicroVM::MicroVM` reads above. `infrastructure.cluster.x-k8s.io`
|
|
67
|
+
// (CAPA's AWSManagedCluster, AWSManagedMachinePool, AWSClusterControllerIdentity)
|
|
68
|
+
// and `controlplane.cluster.x-k8s.io` (CAPA's AWSManagedControlPlane) are left on
|
|
69
|
+
// the first-segment rule — their kinds already carry an `AWS` prefix, so
|
|
70
|
+
// `K8s::Infrastructure::AWSManagedCluster` / `K8s::Controlplane::AWSManagedControlPlane`
|
|
71
|
+
// don't need the same rescue. `addons.cluster.x-k8s.io` (CAAPH's HelmChartProxy,
|
|
72
|
+
// core CAPI's ClusterResourceSet) likewise stays `Addons`.
|
|
73
|
+
"cluster.x-k8s.io": "CAPI",
|
|
74
|
+
// The first-segment rule would give `Opentelemetry`, which misspells the
|
|
75
|
+
// product (OpenTelemetry is one word with two capitals) and leaves the
|
|
76
|
+
// operator's kinds as `K8s::Opentelemetry::OpenTelemetryCollector`.
|
|
77
|
+
"opentelemetry.io": "OpenTelemetry",
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Normalize an API group to its PascalCase namespace segment.
|
|
81
|
+
* "" → "Core", overrides first ("argoproj.io" → "Argo"), then the first
|
|
82
|
+
* dot-segment kebab→PascalCased: "cert-manager.io" → "CertManager",
|
|
83
|
+
* "rbac.authorization.k8s.io" → "Rbac", "apps" → "Apps".
|
|
84
|
+
*/
|
|
85
|
+
export function namespaceSegmentForGroup(group) {
|
|
86
|
+
if (!group || group === "")
|
|
87
|
+
return "Core";
|
|
88
|
+
const override = GROUP_NAMESPACE_OVERRIDES[group];
|
|
89
|
+
if (override)
|
|
90
|
+
return override;
|
|
91
|
+
const firstSegment = group.split(".")[0];
|
|
92
|
+
return firstSegment
|
|
93
|
+
.split("-")
|
|
94
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
95
|
+
.join("");
|
|
96
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Kubernetes manifest embeds another lexicon's content, for
|
|
3
|
+
* `chant import` (#2962).
|
|
4
|
+
*
|
|
5
|
+
* The parser offers two kinds of place to core's embedded-content resolver:
|
|
6
|
+
*
|
|
7
|
+
* - every `data` value of a ConfigMap, as text (a collector's `config.yaml`,
|
|
8
|
+
* a Prometheus rule file, an `alertmanager.yml`, a Grafana dashboard's
|
|
9
|
+
* JSON);
|
|
10
|
+
* - a `monitoring.coreos.com` PrometheusRule's `spec.groups`, as the rule file
|
|
11
|
+
* `{ groups }` those groups would make;
|
|
12
|
+
* - an OpenTelemetry Operator `OpenTelemetryCollector`'s `spec.config` (v1beta1,
|
|
13
|
+
* a structured object), offered as `{ config, header }` with the `# chant:`
|
|
14
|
+
* header lines kept in the `otel.chant.dev/header` annotation (#3367);
|
|
15
|
+
* - a Grafana Operator `GrafanaDashboard`'s `spec.json`, as text (#3015);
|
|
16
|
+
* - the alerting content of the Grafana Operator's `GrafanaAlertRuleGroup`
|
|
17
|
+
* (`spec.rules`), `GrafanaContactPoint` (`spec.receivers`),
|
|
18
|
+
* `GrafanaNotificationPolicy` (`spec.route`), `GrafanaNotificationPolicyRoute`
|
|
19
|
+
* (the whole `spec`, offered as `route`), `GrafanaMuteTiming`
|
|
20
|
+
* (`spec.time_intervals`) and `GrafanaNotificationTemplate`
|
|
21
|
+
* (`spec.template`), each with the whole `spec` as the document so the
|
|
22
|
+
* owner can read the fields beside it (#3538).
|
|
23
|
+
*
|
|
24
|
+
* Which lexicon imports the content is decided by core at run time, from the
|
|
25
|
+
* lexicons that register an `embeddedImporters()`; this module names none of
|
|
26
|
+
* them as a dependency. The `expectedOwner` hints below are the conventions
|
|
27
|
+
* k8s manifests follow, used only to warn when the owner is not installed.
|
|
28
|
+
*/
|
|
29
|
+
import { embeddedDocument } from "@intentius/chant/import/embedded";
|
|
30
|
+
function isObject(v) {
|
|
31
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
32
|
+
}
|
|
33
|
+
/** The annotation `OtelCollectorCR` keeps the collector config's `# chant:` header lines in (see OTEL_COLLECTOR_ANNOTATIONS). */
|
|
34
|
+
const OTEL_HEADER_ANNOTATION = "otel.chant.dev/header";
|
|
35
|
+
/** The `grafana_dashboard` label the Grafana dashboard sidecar looks for (kube-prometheus-stack, the Grafana Helm chart). */
|
|
36
|
+
export const GRAFANA_DASHBOARD_LABEL = "grafana_dashboard";
|
|
37
|
+
/** The Grafana Operator alerting kinds, and the `spec` field holding what the grafana lexicon imports. */
|
|
38
|
+
const GRAFANA_ALERTING_FIELDS = {
|
|
39
|
+
"K8s::Grafana::GrafanaAlertRuleGroup": { field: "rules", what: "Grafana alert rules" },
|
|
40
|
+
"K8s::Grafana::GrafanaContactPoint": { field: "receivers", what: "Grafana contact point receivers" },
|
|
41
|
+
"K8s::Grafana::GrafanaNotificationPolicy": { field: "route", what: "a Grafana notification policy" },
|
|
42
|
+
"K8s::Grafana::GrafanaMuteTiming": { field: "time_intervals", what: "Grafana mute timing intervals" },
|
|
43
|
+
"K8s::Grafana::GrafanaNotificationTemplate": { field: "template", what: "a Grafana notification template" },
|
|
44
|
+
};
|
|
45
|
+
function looksLikeRuleGroups(groups) {
|
|
46
|
+
return Array.isArray(groups) && groups.length > 0 && groups.every((g) => isObject(g) && typeof g.name === "string" && Array.isArray(g.rules));
|
|
47
|
+
}
|
|
48
|
+
/** Whose content a ConfigMap value looks like, by the conventions manifests follow. */
|
|
49
|
+
function hintFor(key, doc, labels) {
|
|
50
|
+
if (!isObject(doc))
|
|
51
|
+
return undefined;
|
|
52
|
+
if (Array.isArray(doc.panels) && (GRAFANA_DASHBOARD_LABEL in labels || key.endsWith(".json"))) {
|
|
53
|
+
return { lexicon: "grafana", what: "a Grafana dashboard" };
|
|
54
|
+
}
|
|
55
|
+
const v2Dashboard = (doc.kind === "Dashboard" && typeof doc.apiVersion === "string" && doc.apiVersion.startsWith("dashboard.grafana.app/")) ||
|
|
56
|
+
(isObject(doc.elements) && isObject(doc.layout));
|
|
57
|
+
if (v2Dashboard && (GRAFANA_DASHBOARD_LABEL in labels || key.endsWith(".json"))) {
|
|
58
|
+
return { lexicon: "grafana", what: "a Grafana dashboard" };
|
|
59
|
+
}
|
|
60
|
+
if (isObject(doc.service) && isObject(doc.service.pipelines)) {
|
|
61
|
+
return { lexicon: "otel", what: "an OpenTelemetry Collector config" };
|
|
62
|
+
}
|
|
63
|
+
if (looksLikeRuleGroups(doc.groups))
|
|
64
|
+
return { lexicon: "prometheus", what: "a Prometheus rule file" };
|
|
65
|
+
if ((isObject(doc.route) || Array.isArray(doc.receivers)) && !("apiVersion" in doc) && !("kind" in doc)) {
|
|
66
|
+
return { lexicon: "prometheus", what: "an Alertmanager config" };
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function stringLabels(metadata) {
|
|
71
|
+
const labels = isObject(metadata) && isObject(metadata.labels) ? metadata.labels : {};
|
|
72
|
+
return Object.fromEntries(Object.entries(labels).filter((e) => typeof e[1] === "string"));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Offer the embedded content of one resource to `embedded`, replacing each
|
|
76
|
+
* value an owner imports with the reference it returns. `properties` is the
|
|
77
|
+
* resource's IR properties, changed in place (with fresh copies of the
|
|
78
|
+
* objects on the way down).
|
|
79
|
+
*/
|
|
80
|
+
export function delegateEmbedded(type, kind, properties, embedded) {
|
|
81
|
+
const metadata = properties.metadata;
|
|
82
|
+
const name = isObject(metadata) && typeof metadata.name === "string" ? metadata.name : kind.toLowerCase();
|
|
83
|
+
const labels = stringLabels(metadata);
|
|
84
|
+
if (type === "K8s::Core::ConfigMap" && isObject(properties.data)) {
|
|
85
|
+
const keys = Object.keys(properties.data);
|
|
86
|
+
const data = { ...properties.data };
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const text = data[key];
|
|
89
|
+
if (typeof text !== "string")
|
|
90
|
+
continue;
|
|
91
|
+
const doc = embeddedDocument(text);
|
|
92
|
+
if (!isObject(doc))
|
|
93
|
+
continue;
|
|
94
|
+
const ref = embedded.resolve({
|
|
95
|
+
host: "k8s",
|
|
96
|
+
hostType: type,
|
|
97
|
+
location: `ConfigMap ${name} data[${JSON.stringify(key)}]`,
|
|
98
|
+
directory: keys.length === 1 ? name : `${name}-${key.replace(/\.[^.]*$/, "")}`,
|
|
99
|
+
text,
|
|
100
|
+
document: doc,
|
|
101
|
+
labels,
|
|
102
|
+
expectedOwner: hintFor(key, doc, labels),
|
|
103
|
+
});
|
|
104
|
+
if (ref)
|
|
105
|
+
data[key] = ref;
|
|
106
|
+
}
|
|
107
|
+
properties.data = data;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (type === "K8s::Grafana::GrafanaDashboard" && isObject(properties.spec) && typeof properties.spec.json === "string") {
|
|
111
|
+
const text = properties.spec.json;
|
|
112
|
+
const doc = embeddedDocument(text);
|
|
113
|
+
if (!isObject(doc))
|
|
114
|
+
return;
|
|
115
|
+
const ref = embedded.resolve({
|
|
116
|
+
host: "k8s",
|
|
117
|
+
hostType: type,
|
|
118
|
+
location: `GrafanaDashboard ${name} spec.json`,
|
|
119
|
+
directory: name,
|
|
120
|
+
text,
|
|
121
|
+
document: doc,
|
|
122
|
+
labels,
|
|
123
|
+
expectedOwner: { lexicon: "grafana", what: "a Grafana dashboard" },
|
|
124
|
+
});
|
|
125
|
+
if (ref)
|
|
126
|
+
properties.spec = { ...properties.spec, json: ref };
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A GrafanaNotificationPolicyRoute's spec is the route itself, so the whole spec is offered (as `route`) and replaced.
|
|
130
|
+
if (type === "K8s::Grafana::GrafanaNotificationPolicyRoute" && isObject(properties.spec) && typeof properties.spec.receiver === "string") {
|
|
131
|
+
const ref = embedded.resolve({
|
|
132
|
+
host: "k8s",
|
|
133
|
+
hostType: type,
|
|
134
|
+
location: `${kind} ${name} spec`,
|
|
135
|
+
directory: name,
|
|
136
|
+
document: { route: properties.spec },
|
|
137
|
+
select: "route",
|
|
138
|
+
labels,
|
|
139
|
+
expectedOwner: { lexicon: "grafana", what: "a Grafana notification policy route" },
|
|
140
|
+
});
|
|
141
|
+
if (ref)
|
|
142
|
+
properties.spec = ref;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const alerting = GRAFANA_ALERTING_FIELDS[type];
|
|
146
|
+
if (alerting && isObject(properties.spec) && properties.spec[alerting.field] !== undefined) {
|
|
147
|
+
const spec = properties.spec;
|
|
148
|
+
const ref = embedded.resolve({
|
|
149
|
+
host: "k8s",
|
|
150
|
+
hostType: type,
|
|
151
|
+
location: `${kind} ${name} spec.${alerting.field}`,
|
|
152
|
+
directory: name,
|
|
153
|
+
document: spec,
|
|
154
|
+
select: alerting.field,
|
|
155
|
+
labels,
|
|
156
|
+
expectedOwner: { lexicon: "grafana", what: alerting.what },
|
|
157
|
+
});
|
|
158
|
+
if (ref)
|
|
159
|
+
properties.spec = { ...spec, [alerting.field]: ref };
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// The CR holds the config as an object, so the owner is asked for an object (`select: "config"`,
|
|
163
|
+
// answered with `collectorConfig([...])`) rather than for text, which would put a string where the CRD wants a map.
|
|
164
|
+
if (type === "K8s::OpenTelemetry::OpenTelemetryCollector" && isObject(properties.spec) && isObject(properties.spec.config)) {
|
|
165
|
+
const spec = properties.spec;
|
|
166
|
+
const raw = isObject(metadata) && isObject(metadata.annotations) ? metadata.annotations[OTEL_HEADER_ANNOTATION] : undefined;
|
|
167
|
+
const header = typeof raw === "string" ? raw.split("\n").filter((l) => l.length > 0) : [];
|
|
168
|
+
const ref = embedded.resolve({
|
|
169
|
+
host: "k8s",
|
|
170
|
+
hostType: type,
|
|
171
|
+
location: `OpenTelemetryCollector ${name} spec.config`,
|
|
172
|
+
directory: name,
|
|
173
|
+
document: { config: spec.config, header },
|
|
174
|
+
select: "config",
|
|
175
|
+
labels,
|
|
176
|
+
expectedOwner: { lexicon: "otel", what: "an OpenTelemetry Collector config" },
|
|
177
|
+
});
|
|
178
|
+
if (ref)
|
|
179
|
+
properties.spec = { ...spec, config: ref };
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (type === "K8s::Monitoring::PrometheusRule" && isObject(properties.spec) && Array.isArray(properties.spec.groups)) {
|
|
183
|
+
const groups = properties.spec.groups;
|
|
184
|
+
const ref = embedded.resolve({
|
|
185
|
+
host: "k8s",
|
|
186
|
+
hostType: type,
|
|
187
|
+
location: `PrometheusRule ${name} spec.groups`,
|
|
188
|
+
directory: name,
|
|
189
|
+
document: { groups },
|
|
190
|
+
select: "groups",
|
|
191
|
+
labels,
|
|
192
|
+
expectedOwner: { lexicon: "prometheus", what: "Prometheus rule groups" },
|
|
193
|
+
});
|
|
194
|
+
if (ref)
|
|
195
|
+
properties.spec = { ...properties.spec, groups: ref };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript code generator for Kubernetes import.
|
|
3
|
+
*
|
|
4
|
+
* Converts a TemplateIR (from parsed K8s manifests) into TypeScript
|
|
5
|
+
* source code using the @intentius/chant-lexicon-k8s constructors.
|
|
6
|
+
*/
|
|
7
|
+
import { EmbeddedImportScope, isEmbeddedReference, renderEmbeddedReference, } from "@intentius/chant/import/embedded";
|
|
8
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
9
|
+
/** An object key as TypeScript: bare when it is an identifier, quoted otherwise (`"config.yaml"`, `"app.kubernetes.io/name"`). */
|
|
10
|
+
function propertyKey(key) {
|
|
11
|
+
return IDENT.test(key) ? key : JSON.stringify(key);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Map K8s entity types to their constructor class names.
|
|
15
|
+
*/
|
|
16
|
+
const TYPE_TO_CLASS = {
|
|
17
|
+
// Core
|
|
18
|
+
"K8s::Core::Pod": "Pod",
|
|
19
|
+
"K8s::Core::Service": "Service",
|
|
20
|
+
"K8s::Core::ConfigMap": "ConfigMap",
|
|
21
|
+
"K8s::Core::Secret": "Secret",
|
|
22
|
+
"K8s::Core::Namespace": "Namespace",
|
|
23
|
+
"K8s::Core::ServiceAccount": "ServiceAccount",
|
|
24
|
+
"K8s::Core::PersistentVolume": "PersistentVolume",
|
|
25
|
+
"K8s::Core::PersistentVolumeClaim": "PersistentVolumeClaim",
|
|
26
|
+
"K8s::Core::Endpoints": "Endpoints",
|
|
27
|
+
"K8s::Core::LimitRange": "LimitRange",
|
|
28
|
+
"K8s::Core::ResourceQuota": "ResourceQuota",
|
|
29
|
+
// Apps
|
|
30
|
+
"K8s::Apps::Deployment": "Deployment",
|
|
31
|
+
"K8s::Apps::StatefulSet": "StatefulSet",
|
|
32
|
+
"K8s::Apps::DaemonSet": "DaemonSet",
|
|
33
|
+
"K8s::Apps::ReplicaSet": "ReplicaSet",
|
|
34
|
+
// Batch
|
|
35
|
+
"K8s::Batch::Job": "Job",
|
|
36
|
+
"K8s::Batch::CronJob": "CronJob",
|
|
37
|
+
// Networking
|
|
38
|
+
"K8s::Networking::Ingress": "Ingress",
|
|
39
|
+
"K8s::Networking::NetworkPolicy": "NetworkPolicy",
|
|
40
|
+
"K8s::Networking::IngressClass": "IngressClass",
|
|
41
|
+
// RBAC
|
|
42
|
+
"K8s::Rbac::Role": "Role",
|
|
43
|
+
"K8s::Rbac::ClusterRole": "ClusterRole",
|
|
44
|
+
"K8s::Rbac::RoleBinding": "RoleBinding",
|
|
45
|
+
"K8s::Rbac::ClusterRoleBinding": "ClusterRoleBinding",
|
|
46
|
+
// Autoscaling
|
|
47
|
+
"K8s::Autoscaling::HorizontalPodAutoscaler": "HorizontalPodAutoscaler",
|
|
48
|
+
// Policy
|
|
49
|
+
"K8s::Policy::PodDisruptionBudget": "PodDisruptionBudget",
|
|
50
|
+
// Storage
|
|
51
|
+
"K8s::Storage::StorageClass": "StorageClass",
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Properties that reference known property entity constructors.
|
|
55
|
+
* Key: property name found in K8s specs → Value: constructor class name.
|
|
56
|
+
*/
|
|
57
|
+
const PROPERTY_CONSTRUCTORS = {
|
|
58
|
+
containers: "Container",
|
|
59
|
+
initContainers: "Container",
|
|
60
|
+
ephemeralContainers: "EphemeralContainer",
|
|
61
|
+
volumes: "Volume",
|
|
62
|
+
volumeMounts: "VolumeMount",
|
|
63
|
+
ports: "ContainerPort",
|
|
64
|
+
env: "EnvVar",
|
|
65
|
+
envFrom: "EnvFromSource",
|
|
66
|
+
resources: "ResourceRequirements",
|
|
67
|
+
securityContext: "SecurityContext",
|
|
68
|
+
podSecurityContext: "PodSecurityContext",
|
|
69
|
+
livenessProbe: "Probe",
|
|
70
|
+
readinessProbe: "Probe",
|
|
71
|
+
startupProbe: "Probe",
|
|
72
|
+
selector: "LabelSelector",
|
|
73
|
+
template: "PodTemplateSpec",
|
|
74
|
+
strategy: "DeploymentStrategy",
|
|
75
|
+
affinity: "Affinity",
|
|
76
|
+
tolerations: "Toleration",
|
|
77
|
+
topologySpreadConstraints: "TopologySpreadConstraint",
|
|
78
|
+
rules: "IngressRule",
|
|
79
|
+
tls: "IngressTLS",
|
|
80
|
+
ingress: "NetworkPolicyIngressRule",
|
|
81
|
+
egress: "NetworkPolicyEgressRule",
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Service port properties need distinct handling since K8s uses "ports"
|
|
85
|
+
* for both ContainerPort and ServicePort depending on the parent resource.
|
|
86
|
+
*/
|
|
87
|
+
const SERVICE_PORT_TYPES = new Set([
|
|
88
|
+
"K8s::Core::Service",
|
|
89
|
+
]);
|
|
90
|
+
/**
|
|
91
|
+
* A Role's and a ClusterRole's top-level `rules` are PolicyRules, not the
|
|
92
|
+
* IngressRules {@link PROPERTY_CONSTRUCTORS} maps `rules` to. Since the
|
|
93
|
+
* property classes carry their members (chant #3093), an IngressRule built
|
|
94
|
+
* from `apiGroups`/`verbs` no longer type-checks.
|
|
95
|
+
*/
|
|
96
|
+
const POLICY_RULE_TYPES = new Set([
|
|
97
|
+
"K8s::Rbac::Role",
|
|
98
|
+
"K8s::Rbac::ClusterRole",
|
|
99
|
+
]);
|
|
100
|
+
/** The constructor for `key` on a resource of `parentType`. */
|
|
101
|
+
function constructorFor(key, parentType) {
|
|
102
|
+
if (key === "rules" && POLICY_RULE_TYPES.has(parentType))
|
|
103
|
+
return "PolicyRule";
|
|
104
|
+
return PROPERTY_CONSTRUCTORS[key];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate TypeScript source code from a Kubernetes IR.
|
|
108
|
+
*/
|
|
109
|
+
export class K8sGenerator {
|
|
110
|
+
constructor() {
|
|
111
|
+
/** The imports for embedded content the current `generate()` call references (#2962). */
|
|
112
|
+
this.scope = new EmbeddedImportScope("");
|
|
113
|
+
}
|
|
114
|
+
generate(ir) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
// Collect which constructors are needed
|
|
117
|
+
const usedConstructors = new Set();
|
|
118
|
+
for (const resource of ir.resources) {
|
|
119
|
+
const cls = this.resolveClass(resource.type);
|
|
120
|
+
if (cls)
|
|
121
|
+
usedConstructors.add(cls);
|
|
122
|
+
// Check properties for nested constructors
|
|
123
|
+
this.collectNestedConstructors(resource.properties, usedConstructors, resource.type);
|
|
124
|
+
}
|
|
125
|
+
// Names the embedded content's imports must not take: every constructor this file may use, and its exports.
|
|
126
|
+
this.scope = new EmbeddedImportScope("", [
|
|
127
|
+
...usedConstructors,
|
|
128
|
+
...Object.values(PROPERTY_CONSTRUCTORS),
|
|
129
|
+
"ServicePort",
|
|
130
|
+
"PolicyRule",
|
|
131
|
+
...ir.resources.map((r) => r.logicalId),
|
|
132
|
+
]);
|
|
133
|
+
// Emit namespaces as comments
|
|
134
|
+
if (ir.metadata?.namespaces && Array.isArray(ir.metadata.namespaces)) {
|
|
135
|
+
lines.push(`// Namespaces: ${ir.metadata.namespaces.join(", ")}`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
}
|
|
138
|
+
// Emit resources
|
|
139
|
+
for (const resource of ir.resources) {
|
|
140
|
+
const cls = this.resolveClass(resource.type);
|
|
141
|
+
if (!cls) {
|
|
142
|
+
lines.push(`// TODO: unsupported type ${resource.type} (${resource.logicalId})`);
|
|
143
|
+
lines.push("");
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const varName = resource.logicalId;
|
|
147
|
+
const propsStr = this.emitProps(resource.properties, 1, resource.type);
|
|
148
|
+
lines.push(`export const ${varName} = new ${cls}(${propsStr});`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
// Import statements: the k8s constructors, then what embedded content references.
|
|
152
|
+
// The constructors are read back from what was emitted (string literals
|
|
153
|
+
// taken out), so a nested one the walk above does not reach (a
|
|
154
|
+
// container's env) is imported too.
|
|
155
|
+
const code = lines.join("\n").replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
|
|
156
|
+
for (const m of code.matchAll(/\bnew ([A-Z][A-Za-z0-9_]*)\(/g))
|
|
157
|
+
usedConstructors.add(m[1]);
|
|
158
|
+
const imports = [...usedConstructors].sort().join(", ");
|
|
159
|
+
const head = [`import { ${imports} } from "@intentius/chant-lexicon-k8s";`, ...this.scope.lines(), ""];
|
|
160
|
+
return [{ path: "main.ts", content: [...head, ...lines].join("\n") }];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a K8s type name to a constructor class name.
|
|
164
|
+
* Falls back to extracting the last segment of the type.
|
|
165
|
+
*/
|
|
166
|
+
resolveClass(type) {
|
|
167
|
+
if (TYPE_TO_CLASS[type])
|
|
168
|
+
return TYPE_TO_CLASS[type];
|
|
169
|
+
// Fallback: extract Kind from "K8s::Group::Kind"
|
|
170
|
+
const parts = type.split("::");
|
|
171
|
+
if (parts.length === 3 && parts[0] === "K8s")
|
|
172
|
+
return parts[2];
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
collectNestedConstructors(props, used, parentType) {
|
|
176
|
+
for (const [key, value] of Object.entries(props)) {
|
|
177
|
+
if (value === undefined || value === null)
|
|
178
|
+
continue;
|
|
179
|
+
// Special case: "ports" on Service resources → ServicePort
|
|
180
|
+
if (key === "ports" && SERVICE_PORT_TYPES.has(parentType)) {
|
|
181
|
+
if (Array.isArray(value))
|
|
182
|
+
used.add("ServicePort");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const constructor = constructorFor(key, parentType);
|
|
186
|
+
if (!constructor)
|
|
187
|
+
continue;
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
used.add(constructor);
|
|
190
|
+
}
|
|
191
|
+
else if (typeof value === "object") {
|
|
192
|
+
used.add(constructor);
|
|
193
|
+
}
|
|
194
|
+
// Recurse into nested specs (e.g., template.spec.containers)
|
|
195
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
196
|
+
this.collectNestedConstructors(value, used, parentType);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Recurse into spec if present
|
|
200
|
+
if (props.spec && typeof props.spec === "object" && !Array.isArray(props.spec)) {
|
|
201
|
+
this.collectNestedConstructors(props.spec, used, parentType);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
emitProps(props, depth, parentType) {
|
|
205
|
+
const indent = " ".repeat(depth);
|
|
206
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
207
|
+
const entries = [];
|
|
208
|
+
for (const [key, value] of Object.entries(props)) {
|
|
209
|
+
if (value === undefined || value === null)
|
|
210
|
+
continue;
|
|
211
|
+
const emitted = this.emitValue(key, value, depth + 1, parentType);
|
|
212
|
+
entries.push(`${innerIndent}${propertyKey(key)}: ${emitted},`);
|
|
213
|
+
}
|
|
214
|
+
if (entries.length === 0)
|
|
215
|
+
return "{}";
|
|
216
|
+
return `{\n${entries.join("\n")}\n${indent}}`;
|
|
217
|
+
}
|
|
218
|
+
emitValue(key, value, depth, parentType) {
|
|
219
|
+
if (value === null || value === undefined)
|
|
220
|
+
return "undefined";
|
|
221
|
+
if (isEmbeddedReference(value))
|
|
222
|
+
return renderEmbeddedReference(value, this.scope, depth * 2);
|
|
223
|
+
// Special case: "ports" on Service → ServicePort constructor
|
|
224
|
+
if (key === "ports" && SERVICE_PORT_TYPES.has(parentType) && Array.isArray(value)) {
|
|
225
|
+
return this.emitConstructorArray("ServicePort", value, depth, parentType);
|
|
226
|
+
}
|
|
227
|
+
// Check if this key maps to a property constructor
|
|
228
|
+
const constructor = constructorFor(key, parentType);
|
|
229
|
+
if (constructor) {
|
|
230
|
+
// Array of constructors (containers, volumes, ports, env, etc.)
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
return this.emitConstructorArray(constructor, value, depth, parentType);
|
|
233
|
+
}
|
|
234
|
+
// Single constructor (securityContext, resources, selector, template, strategy, etc.)
|
|
235
|
+
if (typeof value === "object") {
|
|
236
|
+
const propsStr = this.emitProps(value, depth, parentType);
|
|
237
|
+
return `new ${constructor}(${propsStr})`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return this.emitLiteral(value, depth, parentType);
|
|
241
|
+
}
|
|
242
|
+
emitConstructorArray(constructor, items, depth, parentType) {
|
|
243
|
+
const indent = " ".repeat(depth);
|
|
244
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
245
|
+
const emitted = items.map((item) => {
|
|
246
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
247
|
+
const propsStr = this.emitProps(item, depth + 1, parentType);
|
|
248
|
+
return `new ${constructor}(${propsStr})`;
|
|
249
|
+
}
|
|
250
|
+
return JSON.stringify(item);
|
|
251
|
+
});
|
|
252
|
+
if (emitted.length === 1 && emitted[0].length < 60) {
|
|
253
|
+
return `[${emitted[0]}]`;
|
|
254
|
+
}
|
|
255
|
+
return `[\n${emitted.map((e) => `${innerIndent}${e},`).join("\n")}\n${indent}]`;
|
|
256
|
+
}
|
|
257
|
+
emitLiteral(value, depth, parentType) {
|
|
258
|
+
if (value === null || value === undefined)
|
|
259
|
+
return "undefined";
|
|
260
|
+
if (isEmbeddedReference(value))
|
|
261
|
+
return renderEmbeddedReference(value, this.scope, depth * 2);
|
|
262
|
+
if (typeof value === "string")
|
|
263
|
+
return JSON.stringify(value);
|
|
264
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
265
|
+
return String(value);
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
if (value.length === 0)
|
|
268
|
+
return "[]";
|
|
269
|
+
const items = value.map((item) => this.emitLiteral(item, depth + 1, parentType));
|
|
270
|
+
const oneLine = `[${items.join(", ")}]`;
|
|
271
|
+
if (oneLine.length < 80)
|
|
272
|
+
return oneLine;
|
|
273
|
+
const indent = " ".repeat(depth);
|
|
274
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
275
|
+
return `[\n${items.map((i) => `${innerIndent}${i},`).join("\n")}\n${indent}]`;
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === "object") {
|
|
278
|
+
const entries = Object.entries(value);
|
|
279
|
+
if (entries.length === 0)
|
|
280
|
+
return "{}";
|
|
281
|
+
const indent = " ".repeat(depth);
|
|
282
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
283
|
+
const items = entries.map(([k, v]) => `${innerIndent}${propertyKey(k)}: ${this.emitLiteral(v, depth + 1, parentType)},`);
|
|
284
|
+
return `{\n${items.join("\n")}\n${indent}}`;
|
|
285
|
+
}
|
|
286
|
+
return String(value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
2
|
+
import { K8sParser } from "./parser.js";
|
|
3
|
+
/**
|
|
4
|
+
* Server-managed metadata fields that the API server fills in. They are not
|
|
5
|
+
* part of the declared shape and would only add noise to regenerated source,
|
|
6
|
+
* so they are stripped unless `verbatim` is set.
|
|
7
|
+
*/
|
|
8
|
+
const SERVER_METADATA_FIELDS = [
|
|
9
|
+
"managedFields",
|
|
10
|
+
"uid",
|
|
11
|
+
"resourceVersion",
|
|
12
|
+
"generation",
|
|
13
|
+
"creationTimestamp",
|
|
14
|
+
"selfLink",
|
|
15
|
+
"ownerReferences",
|
|
16
|
+
"finalizers",
|
|
17
|
+
];
|
|
18
|
+
/** Annotations the cluster writes back that are not authored config. */
|
|
19
|
+
const SERVER_ANNOTATIONS = [
|
|
20
|
+
"kubectl.kubernetes.io/last-applied-configuration",
|
|
21
|
+
"deployment.kubernetes.io/revision",
|
|
22
|
+
];
|
|
23
|
+
function isRecord(v) {
|
|
24
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Strip `status`, `managedFields`, and other server-written fields to reach the
|
|
28
|
+
* declared shape. Returns a new object; the input is not mutated.
|
|
29
|
+
*/
|
|
30
|
+
export function stripServerFields(obj) {
|
|
31
|
+
const clone = JSON.parse(JSON.stringify(obj));
|
|
32
|
+
delete clone.status;
|
|
33
|
+
if (isRecord(clone.metadata)) {
|
|
34
|
+
const md = clone.metadata;
|
|
35
|
+
for (const f of SERVER_METADATA_FIELDS)
|
|
36
|
+
delete md[f];
|
|
37
|
+
if (isRecord(md.annotations)) {
|
|
38
|
+
for (const a of SERVER_ANNOTATIONS)
|
|
39
|
+
delete md.annotations[a];
|
|
40
|
+
if (Object.keys(md.annotations).length === 0)
|
|
41
|
+
delete md.annotations;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return clone;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build full-fidelity export IR from a list of live Kubernetes objects (the
|
|
48
|
+
* `items` of a `kubectl get -o json` List). Reuses the import K8sParser by
|
|
49
|
+
* feeding it the cleaned objects as JSON documents — JSON is valid YAML, so no
|
|
50
|
+
* separate serializer is needed. Pure: all I/O stays in the caller.
|
|
51
|
+
*
|
|
52
|
+
* The result's `reparse(context)` parses the same objects again with a
|
|
53
|
+
* `ParseContext`, so `chant import --from` hands embedded content (a
|
|
54
|
+
* collector config in a ConfigMap, a PrometheusRule's groups) to the lexicon
|
|
55
|
+
* that owns it, as file import does (#2995).
|
|
56
|
+
*/
|
|
57
|
+
export function buildExportFromObjects(objects, opts = {}) {
|
|
58
|
+
const owning = opts.owned
|
|
59
|
+
? objects.filter((o) => {
|
|
60
|
+
if (!isRecord(o))
|
|
61
|
+
return false;
|
|
62
|
+
const labels = isRecord(o.metadata) ? o.metadata.labels : undefined;
|
|
63
|
+
return hasOwnershipMarker(labels, LABEL_OWNERSHIP_KEYS);
|
|
64
|
+
})
|
|
65
|
+
: objects;
|
|
66
|
+
const cleaned = owning
|
|
67
|
+
.filter(isRecord)
|
|
68
|
+
.map((o) => (opts.verbatim ? o : stripServerFields(o)));
|
|
69
|
+
const content = cleaned.map((o) => JSON.stringify(o, null, 2)).join("\n---\n");
|
|
70
|
+
const parse = (context) => select(new K8sParser().parse(content, context), opts.selector);
|
|
71
|
+
return { ...parse(), reparse: (context) => parse(context) };
|
|
72
|
+
}
|
|
73
|
+
function select(ir, selector) {
|
|
74
|
+
if (!selector || (selector.type === undefined && selector.name === undefined)) {
|
|
75
|
+
return ir;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...ir,
|
|
79
|
+
resources: ir.resources.filter((r) => {
|
|
80
|
+
const liveName = r.metadata?.originalName ?? r.logicalId;
|
|
81
|
+
return ((selector.type === undefined || r.type === selector.type) &&
|
|
82
|
+
(selector.name === undefined || liveName === selector.name));
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|