@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,229 @@
|
|
|
1
|
+
import { sleep } from "@intentius/chant/op";
|
|
2
|
+
import { defaultK8sConnector } from "../../api/connect.js";
|
|
3
|
+
/**
|
|
4
|
+
* The generic default (kstatus-style): a `Ready` condition of `True` and
|
|
5
|
+
* `observedGeneration` caught up. Covers cert-manager, Gateway API, KubeRay,
|
|
6
|
+
* and the CockroachDB operator.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_READINESS = {
|
|
9
|
+
ready: [{ conditionType: "Ready", status: "True" }],
|
|
10
|
+
observedGeneration: true,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Per-resource overrides keyed by `"<group>/<kind>"`. Argo's `Application`
|
|
14
|
+
* reports `health`/`sync`, not a `Ready` condition — the case that proves the
|
|
15
|
+
* override is necessary (#365).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Flux toolkit wedge reasons (#1549): a `Ready=False` with one of these
|
|
19
|
+
* `reason`s does not heal by waiting — a bad build, a failed install, an
|
|
20
|
+
* unreachable or invalid source. The toolkit is kstatus-conformant, so the
|
|
21
|
+
* READY half of every entry below is just the default; the value added is
|
|
22
|
+
* failing fast instead of polling out the full timeout.
|
|
23
|
+
*/
|
|
24
|
+
const FLUX_TERMINAL = (reasons) => ({
|
|
25
|
+
ready: [{ conditionType: "Ready", status: "True" }],
|
|
26
|
+
terminal: [{ conditionType: "Ready", reasonOneOf: reasons }],
|
|
27
|
+
observedGeneration: true,
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Config Connector (CNRM) wedge reasons (#1725): CNRM is kstatus-conformant —
|
|
31
|
+
* `Ready=True` + `observedGeneration` is the correct ready predicate, same as
|
|
32
|
+
* `DEFAULT_READINESS` — so, like `FLUX_TERMINAL`, the value added here is
|
|
33
|
+
* failing fast on a `Ready=False` reason that will not heal by waiting.
|
|
34
|
+
* `UpdateFailed` is CNRM giving up on a reconcile (bad spec, quota, API
|
|
35
|
+
* error); `ManagementConflict` is two controllers fighting over the same GCP
|
|
36
|
+
* resource. Neither resolves on its own.
|
|
37
|
+
*
|
|
38
|
+
* `DependencyNotReady` is deliberately excluded: it is the normal state while
|
|
39
|
+
* a resource waits on another one it depends on (e.g. a `ContainerCluster`
|
|
40
|
+
* waiting on its network) — exactly the "still being created" case this spec
|
|
41
|
+
* has to distinguish from a real wedge, not fold into it.
|
|
42
|
+
*
|
|
43
|
+
* One entry per kind, matching `FLUX_TERMINAL`'s convention, rather than a
|
|
44
|
+
* group-level key: CNRM's kinds don't share a single apiGroup (`ContainerCluster`
|
|
45
|
+
* and `ContainerNodePool` are `container.cnrm.cloud.google.com`; `SQLInstance`
|
|
46
|
+
* would be `sql.cnrm.cloud.google.com`, etc.), so a group-level key would need
|
|
47
|
+
* prefix matching against `*.cnrm.cloud.google.com` — a real change to
|
|
48
|
+
* `readinessFor`'s exact-match lookup for a handful of entries. Not worth it
|
|
49
|
+
* until more CNRM kinds actually need the override.
|
|
50
|
+
*/
|
|
51
|
+
const CNRM_TERMINAL = () => FLUX_TERMINAL(["UpdateFailed", "ManagementConflict"]);
|
|
52
|
+
export const READINESS_OVERRIDES = {
|
|
53
|
+
"argoproj.io/Application": {
|
|
54
|
+
ready: [
|
|
55
|
+
{ path: "status.health.status", equals: "Healthy" },
|
|
56
|
+
{ path: "status.sync.status", equals: "Synced" },
|
|
57
|
+
],
|
|
58
|
+
terminal: [{ path: "status.health.status", oneOf: ["Degraded", "Missing"] }],
|
|
59
|
+
observedGeneration: false,
|
|
60
|
+
},
|
|
61
|
+
"kustomize.toolkit.fluxcd.io/Kustomization": FLUX_TERMINAL([
|
|
62
|
+
"BuildFailed",
|
|
63
|
+
"HealthCheckFailed",
|
|
64
|
+
"ReconciliationFailed",
|
|
65
|
+
"ArtifactFailed",
|
|
66
|
+
"PruneFailed",
|
|
67
|
+
]),
|
|
68
|
+
"helm.toolkit.fluxcd.io/HelmRelease": FLUX_TERMINAL([
|
|
69
|
+
"InstallFailed",
|
|
70
|
+
"UpgradeFailed",
|
|
71
|
+
"RollbackFailed",
|
|
72
|
+
"UninstallFailed",
|
|
73
|
+
"ArtifactFailed",
|
|
74
|
+
"ChartPullError",
|
|
75
|
+
]),
|
|
76
|
+
"source.toolkit.fluxcd.io/GitRepository": FLUX_TERMINAL([
|
|
77
|
+
"AuthenticationFailed",
|
|
78
|
+
"GitOperationFailed",
|
|
79
|
+
"URLInvalid",
|
|
80
|
+
"StorageOperationFailed",
|
|
81
|
+
]),
|
|
82
|
+
"source.toolkit.fluxcd.io/OCIRepository": FLUX_TERMINAL([
|
|
83
|
+
"AuthenticationFailed",
|
|
84
|
+
"OCIPullFailed",
|
|
85
|
+
"URLInvalid",
|
|
86
|
+
"StorageOperationFailed",
|
|
87
|
+
]),
|
|
88
|
+
"source.toolkit.fluxcd.io/HelmChart": FLUX_TERMINAL([
|
|
89
|
+
"ChartPullError",
|
|
90
|
+
"ChartPackageError",
|
|
91
|
+
"StorageOperationFailed",
|
|
92
|
+
]),
|
|
93
|
+
"container.cnrm.cloud.google.com/ContainerCluster": CNRM_TERMINAL(),
|
|
94
|
+
"container.cnrm.cloud.google.com/ContainerNodePool": CNRM_TERMINAL(),
|
|
95
|
+
};
|
|
96
|
+
/** Resolve the readiness spec for a resource: registry override, else default. */
|
|
97
|
+
export function readinessFor(group, kind) {
|
|
98
|
+
return READINESS_OVERRIDES[`${group ?? ""}/${kind}`] ?? DEFAULT_READINESS;
|
|
99
|
+
}
|
|
100
|
+
// ── Predicate evaluation ────────────────────────────────────────────
|
|
101
|
+
function getPath(obj, path) {
|
|
102
|
+
return path.split(".").reduce((o, k) => (o != null && typeof o === "object" ? o[k] : undefined), obj);
|
|
103
|
+
}
|
|
104
|
+
function isConditionReason(m) {
|
|
105
|
+
return m.reasonOneOf !== undefined;
|
|
106
|
+
}
|
|
107
|
+
function isCondition(m) {
|
|
108
|
+
return m.conditionType !== undefined && !isConditionReason(m);
|
|
109
|
+
}
|
|
110
|
+
function findCondition(obj, conditionType) {
|
|
111
|
+
const conds = getPath(obj, "status.conditions");
|
|
112
|
+
if (!Array.isArray(conds))
|
|
113
|
+
return undefined;
|
|
114
|
+
return conds.find((x) => x && typeof x === "object" && x.type === conditionType);
|
|
115
|
+
}
|
|
116
|
+
function matchCondition(obj, m) {
|
|
117
|
+
const c = findCondition(obj, m.conditionType);
|
|
118
|
+
return !!c && String(c.status) === (m.status ?? "True");
|
|
119
|
+
}
|
|
120
|
+
function matchConditionReason(obj, m) {
|
|
121
|
+
const c = findCondition(obj, m.conditionType);
|
|
122
|
+
if (!c || String(c.status) !== (m.status ?? "False"))
|
|
123
|
+
return false;
|
|
124
|
+
return typeof c.reason === "string" && m.reasonOneOf.includes(c.reason);
|
|
125
|
+
}
|
|
126
|
+
function matchPath(obj, m) {
|
|
127
|
+
const v = getPath(obj, m.path);
|
|
128
|
+
if (m.equals !== undefined)
|
|
129
|
+
return v === m.equals;
|
|
130
|
+
if (m.oneOf !== undefined)
|
|
131
|
+
return m.oneOf.includes(v);
|
|
132
|
+
return v !== undefined && v !== null;
|
|
133
|
+
}
|
|
134
|
+
function matches(obj, m) {
|
|
135
|
+
if (isConditionReason(m))
|
|
136
|
+
return matchConditionReason(obj, m);
|
|
137
|
+
return isCondition(m) ? matchCondition(obj, m) : matchPath(obj, m);
|
|
138
|
+
}
|
|
139
|
+
function observedGenerationReady(obj) {
|
|
140
|
+
const gen = getPath(obj, "metadata.generation");
|
|
141
|
+
const obs = getPath(obj, "status.observedGeneration");
|
|
142
|
+
if (typeof gen === "number" && typeof obs === "number")
|
|
143
|
+
return obs >= gen;
|
|
144
|
+
return true; // absent on either side → don't block
|
|
145
|
+
}
|
|
146
|
+
/** True when every `ready` predicate holds (and observedGeneration is caught up). */
|
|
147
|
+
export function isReady(obj, spec) {
|
|
148
|
+
if (spec.observedGeneration !== false && !observedGenerationReady(obj))
|
|
149
|
+
return false;
|
|
150
|
+
return spec.ready.every((m) => matches(obj, m));
|
|
151
|
+
}
|
|
152
|
+
/** The first matching `terminal` predicate, if any — the resource is wedged. */
|
|
153
|
+
export function firstTerminal(obj, spec) {
|
|
154
|
+
return spec.terminal?.find((m) => matches(obj, m));
|
|
155
|
+
}
|
|
156
|
+
// ── Activity ────────────────────────────────────────────────────────
|
|
157
|
+
/** Thrown when the resource reaches a terminal state. Mark non-retryable. */
|
|
158
|
+
export class ReadinessFailedError extends Error {
|
|
159
|
+
constructor(message) {
|
|
160
|
+
super(message);
|
|
161
|
+
this.name = "ReadinessFailedError";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Read the resource through the typed API client.
|
|
166
|
+
*
|
|
167
|
+
* A client is built once per `waitForReady` call and reused for every poll, so
|
|
168
|
+
* a 20-minute wait does not re-parse the kubeconfig or re-invoke an exec
|
|
169
|
+
* credential plugin on each iteration — and neither does it re-run discovery,
|
|
170
|
+
* which the client caches per API version.
|
|
171
|
+
*/
|
|
172
|
+
export function apiResourceFetcher(connect = defaultK8sConnector) {
|
|
173
|
+
let connection;
|
|
174
|
+
let resolved;
|
|
175
|
+
return async (args, signal) => {
|
|
176
|
+
connection ?? (connection = connect({
|
|
177
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
178
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
179
|
+
}));
|
|
180
|
+
const { client } = await connection;
|
|
181
|
+
if (!resolved) {
|
|
182
|
+
const info = await client.resolve({ resource: args.kind, ...(args.group ? { group: args.group } : {}) }, signal);
|
|
183
|
+
if (!info) {
|
|
184
|
+
throw new Error(`waitForReady: the cluster's API discovery reports no resource matching "${args.kind}"` +
|
|
185
|
+
`${args.group ? ` in group "${args.group}"` : ""} — nothing to wait for`);
|
|
186
|
+
}
|
|
187
|
+
resolved = { apiVersion: info.apiVersion, kind: info.kind };
|
|
188
|
+
}
|
|
189
|
+
return (await client.read({
|
|
190
|
+
apiVersion: resolved.apiVersion,
|
|
191
|
+
kind: resolved.kind,
|
|
192
|
+
name: args.name,
|
|
193
|
+
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
194
|
+
}, { signal }));
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The production reader. Each call builds its own fetcher, so nothing is
|
|
199
|
+
* shared between two unrelated waits; a single `waitForReady` passes one
|
|
200
|
+
* fetcher through all of its polls, which is where the caching matters.
|
|
201
|
+
*/
|
|
202
|
+
export const defaultResourceFetcher = (args, signal) => apiResourceFetcher()(args, signal);
|
|
203
|
+
/**
|
|
204
|
+
* Poll until the resource satisfies its readiness spec. Throws
|
|
205
|
+
* `ReadinessFailedError` on a terminal state, which the `k8sWait` profile lists
|
|
206
|
+
* as non-retryable, so a resource that will never become ready fails on the
|
|
207
|
+
* first attempt instead of burning the profile's 15m timeout three times.
|
|
208
|
+
*
|
|
209
|
+
* @param fetcher injectable reader (defaults to kubectl). Tests pass a fake to
|
|
210
|
+
* drive not-ready → ready / terminal transitions.
|
|
211
|
+
*/
|
|
212
|
+
export async function waitForReady(args, signal, fetcher = apiResourceFetcher()) {
|
|
213
|
+
const spec = args.spec ?? readinessFor(args.group, args.kind);
|
|
214
|
+
const interval = args.intervalMs ?? 15000;
|
|
215
|
+
let attempt = 0;
|
|
216
|
+
while (true) {
|
|
217
|
+
if (signal?.aborted)
|
|
218
|
+
throw new Error("waitForReady aborted");
|
|
219
|
+
attempt++;
|
|
220
|
+
const obj = await fetcher(args, signal);
|
|
221
|
+
const term = firstTerminal(obj, spec);
|
|
222
|
+
if (term) {
|
|
223
|
+
throw new ReadinessFailedError(`${args.kind}/${args.name} reached a terminal state (${JSON.stringify(term)}) — it will not become ready without intervention.`);
|
|
224
|
+
}
|
|
225
|
+
if (isReady(obj, spec))
|
|
226
|
+
return obj;
|
|
227
|
+
await sleep(interval, signal);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2 — "regenerate the step builders as fully typed wrappers"). Core
|
|
4
|
+
* cannot import a lexicon's `*Args` types (lexicons depend on core, not the
|
|
5
|
+
* other way around — see `packages/core/src/op/activity-contract.ts`'s module
|
|
6
|
+
* doc for the same layering constraint on Stage 1's contracts), so a fully
|
|
7
|
+
* typed `kubectlApply`/`waitForReady`/`ensureSecret` has to live beside the
|
|
8
|
+
* `*Args` interface it derives from: here, next to `./activities/kubectl.ts`
|
|
9
|
+
* etc. `opts`'s type in each wrapper below IS the activity's own `*Args`
|
|
10
|
+
* interface (via `Omit`/`WithStepRefs`) — never a hand-restated mirror — so
|
|
11
|
+
* `kubectlApply`/`waitForReady`/`ensureSecret`'s full option surface (the
|
|
12
|
+
* dozen-plus cross-field options on `kubectlApply` that Stage 1's zod
|
|
13
|
+
* contracts deliberately skipped rather than duplicate) is authoring-time
|
|
14
|
+
* typed for free, and stays in sync with the implementation by construction.
|
|
15
|
+
*
|
|
16
|
+
* Every field also accepts a {@link StepOutputRef} in its place
|
|
17
|
+
* ({@link WithStepRefs}, chant #1950) — a later step's args can consume an
|
|
18
|
+
* earlier step's declared output.
|
|
19
|
+
*
|
|
20
|
+
* `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
|
|
21
|
+
* `@intentius/chant/op`, the lexicon-agnostic single-import barrel) are
|
|
22
|
+
* UNCHANGED and produce byte-identical `ActivityStep` output for the same
|
|
23
|
+
* inputs — these are purely additive. Deliberately not swapped into core's
|
|
24
|
+
* barrel: that would make `@intentius/chant` depend on this package (and on
|
|
25
|
+
* helm, for `helmInstall`) at runtime, so a project that never touches
|
|
26
|
+
* Kubernetes would install both just to import anything from `op`. That is
|
|
27
|
+
* exactly the "core stays product-agnostic" property the
|
|
28
|
+
* `op/activities/index.ts` module docs across this repo call out on purpose
|
|
29
|
+
* (activities live in per-product lexicons for this reason; the step-builder
|
|
30
|
+
* layer shouldn't reintroduce the coupling from the other direction). An
|
|
31
|
+
* author who wants the typed surface imports it from here —
|
|
32
|
+
* `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
|
|
33
|
+
* already depends on; existing `@intentius/chant/op` imports keep working
|
|
34
|
+
* exactly as before, opting in only if the import is changed.
|
|
35
|
+
*/
|
|
36
|
+
import { activity, takeProfileAndId, } from "@intentius/chant/op";
|
|
37
|
+
/**
|
|
38
|
+
* Run `kubectl apply -f <manifest>` — the fully typed twin of core's
|
|
39
|
+
* `kubectlApply` step-builder shortcut (chant #1288 Stage 2). `opts` is
|
|
40
|
+
* {@link KubectlApplyArgs} itself (minus the positional `manifest`) — every
|
|
41
|
+
* field `applyManifest` accepts, including `deleteMode`, `force`,
|
|
42
|
+
* `fieldManager`, `documents`, is authoring-time typed here, not just the
|
|
43
|
+
* subset a hand-written zod mirror would bother restating. Defaults to the
|
|
44
|
+
* `longInfra` profile (override via `opts.profile`).
|
|
45
|
+
*/
|
|
46
|
+
export const kubectlApply = (manifest, opts) => {
|
|
47
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
48
|
+
return activity("kubectlApply", { manifest, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Poll a Kubernetes resource until it reports ready, driven by a data-only
|
|
52
|
+
* readiness spec (CRD-aware; #365) — the fully typed twin of core's
|
|
53
|
+
* `waitForReady`. `opts` is {@link WaitForReadyArgs} itself, minus the
|
|
54
|
+
* positional `kind`/`name`. Defaults to the `k8sWait` profile.
|
|
55
|
+
*/
|
|
56
|
+
export const waitForReady = (kind, name, opts) => {
|
|
57
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
58
|
+
return activity("waitForReady", { kind, name, ...args }, { profile: profile ?? "k8sWait", ...(id ? { id } : {}) });
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Ensure a `generated-once` secret exists in the target store (#1829) — the
|
|
62
|
+
* fully typed twin of core's `ensureSecret`. `opts` is
|
|
63
|
+
* {@link EnsureSecretActivityArgs} itself, minus the positional `name`/`keys`.
|
|
64
|
+
*/
|
|
65
|
+
export const ensureSecret = (name, keys, opts) => {
|
|
66
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
67
|
+
return activity("ensureSecret", { name, keys, ...args }, { profile, ...(id ? { id } : {}) });
|
|
68
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for Kubernetes lexicon packaging.
|
|
4
|
+
*/
|
|
5
|
+
import { packageLexicon } from "./codegen/package.js";
|
|
6
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
async function main() {
|
|
11
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
12
|
+
const force = process.argv.includes("--force") || process.argv.includes("-f");
|
|
13
|
+
const { spec, stats } = await packageLexicon({ verbose, force });
|
|
14
|
+
const distDir = join(pkgDir, "..", "dist");
|
|
15
|
+
writeBundleSpec(spec, distDir);
|
|
16
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
17
|
+
}
|
|
18
|
+
main().catch((err) => {
|
|
19
|
+
console.error("Packaging failed:", err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6902 JSON Patch and RFC 7386 JSON Merge Patch, for rendered documents.
|
|
3
|
+
*
|
|
4
|
+
* Core carries an RFC 6902 applier for cfn-lint codegen
|
|
5
|
+
* (`packages/core/src/codegen/json-patch.ts`), but it skips `test`, `move`
|
|
6
|
+
* and `copy`, and it works on JSON strings. A patch on a rendered manifest
|
|
7
|
+
* needs `test` most of all: it is how an author pins the value a chart
|
|
8
|
+
* upgrade might change underneath them. So this module is a small, complete
|
|
9
|
+
* applier over plain objects, with no imports, usable from the build path and
|
|
10
|
+
* from the helm lexicon's `HelmRender`.
|
|
11
|
+
*
|
|
12
|
+
* Both appliers return a new document and never mutate their input.
|
|
13
|
+
*/
|
|
14
|
+
function isObject(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function clone(value) {
|
|
18
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
/** "" -> [], "/a/b~1c" -> ["a", "b/c"]. */
|
|
21
|
+
function parsePointer(pointer) {
|
|
22
|
+
if (pointer === "")
|
|
23
|
+
return [];
|
|
24
|
+
if (!pointer.startsWith("/"))
|
|
25
|
+
throw new Error(`"${pointer}" is not a JSON Pointer (it must start with "/")`);
|
|
26
|
+
return pointer
|
|
27
|
+
.slice(1)
|
|
28
|
+
.split("/")
|
|
29
|
+
.map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
30
|
+
}
|
|
31
|
+
function arrayIndex(arr, token, pointer, allowEnd) {
|
|
32
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token))
|
|
33
|
+
throw new Error(`"${token}" in ${pointer} is not an array index`);
|
|
34
|
+
const idx = Number(token);
|
|
35
|
+
const max = allowEnd ? arr.length : arr.length - 1;
|
|
36
|
+
if (idx > max)
|
|
37
|
+
throw new Error(`index ${idx} in ${pointer} is out of bounds (array length ${arr.length})`);
|
|
38
|
+
return idx;
|
|
39
|
+
}
|
|
40
|
+
function get(root, pointer) {
|
|
41
|
+
let current = root;
|
|
42
|
+
for (const token of parsePointer(pointer)) {
|
|
43
|
+
if (Array.isArray(current)) {
|
|
44
|
+
current = current[arrayIndex(current, token, pointer, false)];
|
|
45
|
+
}
|
|
46
|
+
else if (isObject(current) && Object.prototype.hasOwnProperty.call(current, token)) {
|
|
47
|
+
current = current[token];
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new Error(`path ${pointer} does not exist`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
/** The container a pointer's last token addresses, and that token. */
|
|
56
|
+
function parentOf(root, pointer) {
|
|
57
|
+
const tokens = parsePointer(pointer);
|
|
58
|
+
if (tokens.length === 0)
|
|
59
|
+
throw new Error("the root cannot be addressed as a member");
|
|
60
|
+
const token = tokens.pop();
|
|
61
|
+
const parentPointer = tokens.length === 0 ? "" : "/" + tokens.map((t) => t.replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
|
|
62
|
+
const parent = get(root, parentPointer);
|
|
63
|
+
if (!Array.isArray(parent) && !isObject(parent)) {
|
|
64
|
+
throw new Error(`path ${pointer} does not exist (its parent is not an object or array)`);
|
|
65
|
+
}
|
|
66
|
+
return { parent, token };
|
|
67
|
+
}
|
|
68
|
+
function add(root, pointer, value) {
|
|
69
|
+
if (pointer === "")
|
|
70
|
+
return value;
|
|
71
|
+
const { parent, token } = parentOf(root, pointer);
|
|
72
|
+
if (Array.isArray(parent)) {
|
|
73
|
+
if (token === "-")
|
|
74
|
+
parent.push(value);
|
|
75
|
+
else
|
|
76
|
+
parent.splice(arrayIndex(parent, token, pointer, true), 0, value);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
parent[token] = value;
|
|
80
|
+
}
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
function remove(root, pointer) {
|
|
84
|
+
if (pointer === "")
|
|
85
|
+
throw new Error("the document root cannot be removed");
|
|
86
|
+
const { parent, token } = parentOf(root, pointer);
|
|
87
|
+
if (Array.isArray(parent)) {
|
|
88
|
+
parent.splice(arrayIndex(parent, token, pointer, false), 1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(parent, token))
|
|
92
|
+
throw new Error(`path ${pointer} does not exist`);
|
|
93
|
+
delete parent[token];
|
|
94
|
+
}
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
function replace(root, pointer, value) {
|
|
98
|
+
if (pointer === "")
|
|
99
|
+
return value;
|
|
100
|
+
get(root, pointer); // must exist
|
|
101
|
+
const { parent, token } = parentOf(root, pointer);
|
|
102
|
+
if (Array.isArray(parent))
|
|
103
|
+
parent[arrayIndex(parent, token, pointer, false)] = value;
|
|
104
|
+
else
|
|
105
|
+
parent[token] = value;
|
|
106
|
+
return root;
|
|
107
|
+
}
|
|
108
|
+
function deepEqual(a, b) {
|
|
109
|
+
if (a === b)
|
|
110
|
+
return true;
|
|
111
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
112
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
113
|
+
}
|
|
114
|
+
if (isObject(a) && isObject(b)) {
|
|
115
|
+
const ka = Object.keys(a);
|
|
116
|
+
const kb = Object.keys(b);
|
|
117
|
+
return ka.length === kb.length && ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Apply an RFC 6902 patch. Operations apply in order; the first one that
|
|
123
|
+
* fails (a missing path, a failed `test`) throws with its index, and the
|
|
124
|
+
* input is left untouched.
|
|
125
|
+
*/
|
|
126
|
+
export function applyJsonPatch(doc, operations) {
|
|
127
|
+
let root = clone(doc);
|
|
128
|
+
operations.forEach((operation, i) => {
|
|
129
|
+
try {
|
|
130
|
+
switch (operation.op) {
|
|
131
|
+
case "add":
|
|
132
|
+
root = add(root, operation.path, clone(operation.value));
|
|
133
|
+
break;
|
|
134
|
+
case "remove":
|
|
135
|
+
root = remove(root, operation.path);
|
|
136
|
+
break;
|
|
137
|
+
case "replace":
|
|
138
|
+
root = replace(root, operation.path, clone(operation.value));
|
|
139
|
+
break;
|
|
140
|
+
case "move": {
|
|
141
|
+
if (operation.path.startsWith(operation.from + "/")) {
|
|
142
|
+
throw new Error(`cannot move ${operation.from} into its own child ${operation.path}`);
|
|
143
|
+
}
|
|
144
|
+
const value = get(root, operation.from);
|
|
145
|
+
root = remove(root, operation.from);
|
|
146
|
+
root = add(root, operation.path, value);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "copy":
|
|
150
|
+
root = add(root, operation.path, clone(get(root, operation.from)));
|
|
151
|
+
break;
|
|
152
|
+
case "test": {
|
|
153
|
+
const actual = get(root, operation.path);
|
|
154
|
+
if (!deepEqual(actual, operation.value)) {
|
|
155
|
+
throw new Error(`test failed at ${operation.path}: expected ${JSON.stringify(operation.value)}, found ${JSON.stringify(actual)}`);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`unknown op "${operation.op}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new Error(`operation ${i} (${operation.op}): ${err instanceof Error ? err.message : String(err)}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return root;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Apply an RFC 7386 merge patch: objects merge key by key, a `null` value
|
|
171
|
+
* deletes the key, and anything else (arrays included) replaces the target
|
|
172
|
+
* whole.
|
|
173
|
+
*/
|
|
174
|
+
export function applyMergePatch(doc, patch) {
|
|
175
|
+
if (!isObject(patch))
|
|
176
|
+
return clone(patch);
|
|
177
|
+
const target = isObject(doc) ? clone(doc) : {};
|
|
178
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
179
|
+
if (value === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
if (value === null)
|
|
182
|
+
delete target[key];
|
|
183
|
+
else
|
|
184
|
+
target[key] = applyMergePatch(target[key], value);
|
|
185
|
+
}
|
|
186
|
+
return target;
|
|
187
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { applyJsonPatch, applyMergePatch } from "./json-patch.js";
|
|
2
|
+
export { applyJsonPatch, applyMergePatch } from "./json-patch.js";
|
|
3
|
+
/**
|
|
4
|
+
* Identity helper that infers `kind`, so a `merge` body is checked against
|
|
5
|
+
* that kind's props:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* renderedPatch({ kind: "Deployment", name: "web", merge: { spec: { replicas: 3 } } })
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* A plain object literal works anywhere a `RenderedPatch` is taken; it just
|
|
12
|
+
* types `merge` as an open record.
|
|
13
|
+
*/
|
|
14
|
+
export function renderedPatch(patch) {
|
|
15
|
+
return patch;
|
|
16
|
+
}
|
|
17
|
+
function describeSelector(sel) {
|
|
18
|
+
const parts = [`kind ${sel.kind}`, `name ${sel.name}`];
|
|
19
|
+
if (sel.namespace !== undefined)
|
|
20
|
+
parts.push(`namespace ${sel.namespace}`);
|
|
21
|
+
if (sel.apiVersion !== undefined)
|
|
22
|
+
parts.push(`apiVersion ${sel.apiVersion}`);
|
|
23
|
+
return parts.join(", ");
|
|
24
|
+
}
|
|
25
|
+
function metadataOf(doc) {
|
|
26
|
+
const m = doc.metadata;
|
|
27
|
+
return typeof m === "object" && m !== null && !Array.isArray(m) ? m : {};
|
|
28
|
+
}
|
|
29
|
+
function matches(doc, sel) {
|
|
30
|
+
if (doc.kind !== sel.kind)
|
|
31
|
+
return false;
|
|
32
|
+
if (sel.apiVersion !== undefined && doc.apiVersion !== sel.apiVersion)
|
|
33
|
+
return false;
|
|
34
|
+
const metadata = metadataOf(doc);
|
|
35
|
+
if (metadata.name !== sel.name)
|
|
36
|
+
return false;
|
|
37
|
+
if (sel.namespace !== undefined && metadata.namespace !== sel.namespace)
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Apply `patches` in order to `docs` and return the patched documents, in
|
|
43
|
+
* the same order. `source` names where the documents came from (a chart, a
|
|
44
|
+
* file, a URL, an overlay dir) and leads every error message.
|
|
45
|
+
*
|
|
46
|
+
* Each patch applies to every document its selector matches at that point,
|
|
47
|
+
* so a later patch sees an earlier one's changes. Throws when a selector
|
|
48
|
+
* matches nothing, when an operation fails (a `test` included), when a patch
|
|
49
|
+
* carries both or neither of `jsonPatch` and `merge`, and when a patched
|
|
50
|
+
* document no longer has a string `apiVersion` and `kind`.
|
|
51
|
+
*/
|
|
52
|
+
export function applyRenderedPatches(docs, patches, source) {
|
|
53
|
+
const out = [...docs];
|
|
54
|
+
if (!patches || patches.length === 0)
|
|
55
|
+
return out;
|
|
56
|
+
patches.forEach((patch, p) => {
|
|
57
|
+
const where = `${source}: patch ${p} (${describeSelector(patch)})`;
|
|
58
|
+
const hasJson = patch.jsonPatch !== undefined;
|
|
59
|
+
const hasMerge = patch.merge !== undefined;
|
|
60
|
+
if (hasJson === hasMerge) {
|
|
61
|
+
throw new Error(`${where} must set exactly one of jsonPatch and merge`);
|
|
62
|
+
}
|
|
63
|
+
let matched = 0;
|
|
64
|
+
for (let i = 0; i < out.length; i++) {
|
|
65
|
+
if (!matches(out[i], patch))
|
|
66
|
+
continue;
|
|
67
|
+
matched++;
|
|
68
|
+
let next;
|
|
69
|
+
try {
|
|
70
|
+
next = hasJson ? applyJsonPatch(out[i], patch.jsonPatch) : applyMergePatch(out[i], patch.merge);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new Error(`${where}: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof next !== "object" || next === null || typeof next.apiVersion !== "string" || typeof next.kind !== "string") {
|
|
76
|
+
throw new Error(`${where} left a document without a string apiVersion and kind`);
|
|
77
|
+
}
|
|
78
|
+
out[i] = next;
|
|
79
|
+
}
|
|
80
|
+
if (matched === 0) {
|
|
81
|
+
const sameKind = out
|
|
82
|
+
.filter((d) => d.kind === patch.kind)
|
|
83
|
+
.map((d) => {
|
|
84
|
+
const m = metadataOf(d);
|
|
85
|
+
return typeof m.namespace === "string" ? `${m.namespace}/${String(m.name)}` : String(m.name);
|
|
86
|
+
});
|
|
87
|
+
throw new Error(`${where} matched no document. ` +
|
|
88
|
+
(sameKind.length > 0
|
|
89
|
+
? `${patch.kind} documents present: ${sameKind.join(", ")}.`
|
|
90
|
+
: `There are no ${patch.kind} documents.`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return out;
|
|
94
|
+
}
|