@intentius/chant-lexicon-k8s 0.124.30 → 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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube source` (chant #1079) — resolve a live object back to the
|
|
3
|
+
* `.ts` file and composite that declared it, reusing the exact build
|
|
4
|
+
* provenance the gitlab/forgejo lexicons' own `:source` context tools already
|
|
5
|
+
* expose (`@intentius/chant/provenance`, stamped during discovery — chant
|
|
6
|
+
* #1064). That precedent is explicit that provenance here is **entity-level
|
|
7
|
+
* (file + composite), not a YAML-line source map** — chant does not carry
|
|
8
|
+
* one, for k8s or for anything else — so this verb reports the same two
|
|
9
|
+
* facts (never a fabricated line number) and says so plainly rather than
|
|
10
|
+
* silently coming up one field short of the issue's wording.
|
|
11
|
+
*/
|
|
12
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
13
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
14
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
15
|
+
import { kubeConnect } from "./connect.js";
|
|
16
|
+
import { parseKubeFlags, connectOptionsFrom } from "./flags.js";
|
|
17
|
+
import { loadKubeProjectContext, relativeProvenance } from "./project.js";
|
|
18
|
+
import { resolveKubeTarget, matchLiveObject, isTargetError } from "./target.js";
|
|
19
|
+
function targetLabel(target) {
|
|
20
|
+
return "apiVersion" in target.selector ? target.selector.kind : target.selector.resource;
|
|
21
|
+
}
|
|
22
|
+
export async function runSource(rawArgs, deps = {}) {
|
|
23
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
24
|
+
const loadProject = deps.loadProject ?? loadKubeProjectContext;
|
|
25
|
+
let flags;
|
|
26
|
+
try {
|
|
27
|
+
flags = parseKubeFlags(rawArgs);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const json = flags.values.output === "json";
|
|
34
|
+
const project = await loadProject();
|
|
35
|
+
if (!project) {
|
|
36
|
+
if (json)
|
|
37
|
+
console.log(JSON.stringify({ available: false, note: "not running inside a chant project (no chant.config.ts, or the project has build errors)" }));
|
|
38
|
+
else
|
|
39
|
+
console.log("chant source: unavailable — not running inside a chant project (no chant.config.ts, or the project has build errors)");
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const target = resolveKubeTarget(flags.positional, flags.values.namespace, project);
|
|
43
|
+
if (isTargetError(target)) {
|
|
44
|
+
console.error(`error: ${target.error}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (!target.name) {
|
|
48
|
+
console.error("error: chant kube source requires a single named resource, e.g. `chant kube source deployment web`");
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
52
|
+
if (connected.kind === "unobserved") {
|
|
53
|
+
console.error(formatUnobserved(targetLabel(target), { reason: connected.reason, detail: connected.detail }));
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const { client } = connected;
|
|
57
|
+
let info;
|
|
58
|
+
try {
|
|
59
|
+
info = await client.resolve(target.selector);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const outcome = classifyApiFailure(err);
|
|
63
|
+
console.error(formatUnobserved(targetLabel(target), {
|
|
64
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
65
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
66
|
+
}));
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
if (!info) {
|
|
70
|
+
console.error(`error: the server doesn't have a resource type "${targetLabel(target)}"`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const namespace = target.namespace ?? client.defaultNamespace;
|
|
74
|
+
let obj;
|
|
75
|
+
try {
|
|
76
|
+
obj = await client.readIfPresent({ apiVersion: info.apiVersion, kind: info.kind, name: target.name, namespace });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const outcome = classifyApiFailure(err);
|
|
80
|
+
console.error(formatUnobserved(`${info.kind}/${target.name}`, {
|
|
81
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
82
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
83
|
+
}));
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
if (!obj) {
|
|
87
|
+
console.error(`Error from server (NotFound): ${info.name} "${target.name}" not found`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const match = target.declaredMatch ??
|
|
91
|
+
matchLiveObject(project, {
|
|
92
|
+
apiVersion: obj.apiVersion ?? info.apiVersion,
|
|
93
|
+
kind: obj.kind ?? info.kind,
|
|
94
|
+
name: obj.metadata?.name ?? target.name,
|
|
95
|
+
namespace: obj.metadata?.namespace ?? namespace,
|
|
96
|
+
});
|
|
97
|
+
if (!match) {
|
|
98
|
+
const note = `no declared entity in this project names ${info.kind}/${target.name}`;
|
|
99
|
+
if (json)
|
|
100
|
+
console.log(JSON.stringify({ found: false, note }));
|
|
101
|
+
else
|
|
102
|
+
console.log(`${info.kind}/${target.name}: ${note}`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const prov = relativeProvenance(project, match.entity);
|
|
106
|
+
if (json) {
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
found: true,
|
|
109
|
+
entity: match.entityName,
|
|
110
|
+
file: prov?.sourceFile ?? null,
|
|
111
|
+
composite: prov?.composite ?? null,
|
|
112
|
+
compositeInstance: prov?.compositeInstance ?? null,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(`${info.kind}/${target.name} <- ${match.entityName}`);
|
|
117
|
+
console.log(` file: ${prov?.sourceFile ?? "(unknown)"}`);
|
|
118
|
+
console.log(` composite: ${prov?.composite ?? "(none — declared directly)"}`);
|
|
119
|
+
console.log(` line: not tracked — chant's build provenance is entity-level (file + composite), not a source map`);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving `chant kube <verb> <target...>`'s positional arguments to an
|
|
3
|
+
* addressable resource (chant #1079).
|
|
4
|
+
*
|
|
5
|
+
* "Every verb resolves entity names through chant's own vocabulary where
|
|
6
|
+
* applicable ... while also accepting raw kind/name kubectl-style." Both
|
|
7
|
+
* forms share one positional shape, disambiguated by arity:
|
|
8
|
+
*
|
|
9
|
+
* - One token, and it names a declared entity in this project → chant's own
|
|
10
|
+
* vocabulary: the entity's `metadata.name`(/`namespace`) is looked up in
|
|
11
|
+
* its own source, not typed twice.
|
|
12
|
+
* - One or two tokens otherwise → kubectl's own shape, `<kind> [name]`; a
|
|
13
|
+
* bare kind with no name is a list, matching `kubectl get <kind>`.
|
|
14
|
+
*
|
|
15
|
+
* `<kind>` is never parsed here beyond splitting the `.group` suffix — that
|
|
16
|
+
* job belongs to the typed client's own `resolve()` (kubectl's own
|
|
17
|
+
* plural/singular/kind/short-name precedence, `packages/k8s-client/src/client.ts`),
|
|
18
|
+
* so a raw kind string reaches the cluster exactly as `kubectl get raycluster.ray.io`
|
|
19
|
+
* would.
|
|
20
|
+
*/
|
|
21
|
+
import { findDeclaredMatch } from "./project.js";
|
|
22
|
+
import { operationFor } from "../api/operation-surface.js";
|
|
23
|
+
export function isTargetError(t) {
|
|
24
|
+
return "error" in t;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve `positional` (whatever's left after flags) plus an explicit
|
|
28
|
+
* `-n/--namespace`, against `project`'s declared entities when available.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveKubeTarget(positional, namespaceFlag, project) {
|
|
31
|
+
if (positional.length === 0) {
|
|
32
|
+
return { error: "a resource kind (and, usually, a name) is required — e.g. `chant kube get deployments` or `chant kube get deployment web`" };
|
|
33
|
+
}
|
|
34
|
+
const [first, second] = positional;
|
|
35
|
+
if (project && second === undefined) {
|
|
36
|
+
const entity = project.entities.get(first);
|
|
37
|
+
if (entity && entity.lexicon === "k8s") {
|
|
38
|
+
const operation = operationFor(entity.entityType);
|
|
39
|
+
const props = "props" in entity ? entity.props : undefined;
|
|
40
|
+
const metadata = props?.metadata;
|
|
41
|
+
if (operation && metadata?.name) {
|
|
42
|
+
return {
|
|
43
|
+
selector: { apiVersion: operation.apiVersion, kind: operation.kind },
|
|
44
|
+
name: metadata.name,
|
|
45
|
+
namespace: namespaceFlag ?? metadata.namespace,
|
|
46
|
+
declaredMatch: { entityName: first, entity, props },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { selector: { resource: first }, name: second, namespace: namespaceFlag };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* After a live read resolves an object's real `apiVersion`/`kind` (a kubectl
|
|
55
|
+
* kind string like `deploy` isn't one), reverse-map it against the project
|
|
56
|
+
* for the verdict column / `source` — reusing the exact match `resolveKubeTarget`
|
|
57
|
+
* would have made had the caller spelled out the entity name.
|
|
58
|
+
*/
|
|
59
|
+
export function matchLiveObject(project, ref) {
|
|
60
|
+
if (!project)
|
|
61
|
+
return undefined;
|
|
62
|
+
return findDeclaredMatch(project.entities, ref);
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles for `chant kube` (chant #1079).
|
|
3
|
+
*
|
|
4
|
+
* `./project.ts`'s real `loadKubeProjectContext` builds an actual project
|
|
5
|
+
* (`chant.config.ts` read, source discovered, serialized) — every verb takes
|
|
6
|
+
* it as an injectable dependency for exactly this reason: a unit test builds
|
|
7
|
+
* a `KubeProjectContext` by hand instead, the same way `../api/fake-cluster.ts`
|
|
8
|
+
* lets a test build a client without a socket.
|
|
9
|
+
*/
|
|
10
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
11
|
+
import { setProvenance } from "@intentius/chant/provenance";
|
|
12
|
+
/** A minimal `Declarable` carrying `props`, optionally stamped with build provenance. */
|
|
13
|
+
export function fakeDeclarable(entityType, props, provenance) {
|
|
14
|
+
const entity = {
|
|
15
|
+
lexicon: "k8s",
|
|
16
|
+
entityType,
|
|
17
|
+
kind: "resource",
|
|
18
|
+
props,
|
|
19
|
+
[DECLARABLE_MARKER]: true,
|
|
20
|
+
};
|
|
21
|
+
if (provenance)
|
|
22
|
+
setProvenance(entity, provenance);
|
|
23
|
+
return entity;
|
|
24
|
+
}
|
|
25
|
+
/** A `KubeProjectContext` built from a plain name → entity map, no disk I/O. */
|
|
26
|
+
export function fakeProjectContext(entities, config = {}, cwd = "/project") {
|
|
27
|
+
return { cwd, config, entities: new Map(Object.entries(entities)) };
|
|
28
|
+
}
|
package/dist/kube/top.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube top pods|nodes` (chant #1079).
|
|
3
|
+
*
|
|
4
|
+
* `metrics.k8s.io` is not part of the standard Kubernetes OpenAPI schema chant's
|
|
5
|
+
* codegen ingests — it is an aggregated API a cluster may or may not be running
|
|
6
|
+
* (metrics-server). It needs no entry in the generated operation surface either
|
|
7
|
+
* way: the typed client's kubectl-style selector (`{ resource, group }`) resolves
|
|
8
|
+
* purely off the *cluster's own* discovery, the same path `waitForReady`'s `kind`
|
|
9
|
+
* argument already uses for CRDs — so this needs no client change, only the
|
|
10
|
+
* right selector.
|
|
11
|
+
*
|
|
12
|
+
* Kubectl's own `top` sums each Pod's per-container usage into one CPU/memory
|
|
13
|
+
* figure, which needs real quantity-unit arithmetic (`250m` + `100m`, `128Mi` +
|
|
14
|
+
* `64Mi`). That normalization is not implemented here — each container's raw
|
|
15
|
+
* usage string is shown as the API reported it, unsummed. Good enough for "is
|
|
16
|
+
* anything obviously starved", not a replacement for `kubectl top --containers`.
|
|
17
|
+
*/
|
|
18
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
19
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
20
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
21
|
+
import { kubeConnect } from "./connect.js";
|
|
22
|
+
import { parseKubeFlags, connectOptionsFrom } from "./flags.js";
|
|
23
|
+
import { renderTable } from "./render.js";
|
|
24
|
+
const RESOURCE_ALIASES = {
|
|
25
|
+
pod: "pods",
|
|
26
|
+
pods: "pods",
|
|
27
|
+
po: "pods",
|
|
28
|
+
node: "nodes",
|
|
29
|
+
nodes: "nodes",
|
|
30
|
+
no: "nodes",
|
|
31
|
+
};
|
|
32
|
+
export async function runTop(rawArgs, deps = {}) {
|
|
33
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
34
|
+
let flags;
|
|
35
|
+
try {
|
|
36
|
+
flags = parseKubeFlags(rawArgs);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const resource = RESOURCE_ALIASES[flags.positional[0] ?? "pods"];
|
|
43
|
+
if (!resource) {
|
|
44
|
+
console.error(`error: unknown resource "${flags.positional[0]}" — chant kube top supports "pods" and "nodes"`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
48
|
+
if (connected.kind === "unobserved") {
|
|
49
|
+
console.error(formatUnobserved(resource, { reason: connected.reason, detail: connected.detail }));
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const { client } = connected;
|
|
53
|
+
let info;
|
|
54
|
+
try {
|
|
55
|
+
info = await client.resolve({ resource, group: "metrics.k8s.io" });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const outcome = classifyApiFailure(err);
|
|
59
|
+
console.error(formatUnobserved(resource, {
|
|
60
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
61
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
62
|
+
}));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if (!info) {
|
|
66
|
+
console.error("error: the metrics API (metrics.k8s.io) is not available in this cluster — is metrics-server installed?");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const namespace = resource === "pods" && !flags.flags.allNamespaces ? (flags.values.namespace ?? client.defaultNamespace) : undefined;
|
|
70
|
+
let items;
|
|
71
|
+
try {
|
|
72
|
+
items = await client.list({ apiVersion: info.apiVersion, kind: info.kind }, { ...(namespace ? { namespace } : {}) });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const outcome = classifyApiFailure(err);
|
|
76
|
+
console.error(formatUnobserved(resource, {
|
|
77
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
78
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
79
|
+
}));
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
if (resource === "nodes") {
|
|
83
|
+
const rows = items.map((n) => [
|
|
84
|
+
n.metadata?.name ?? "",
|
|
85
|
+
String(n.usage?.cpu ?? "<unknown>"),
|
|
86
|
+
String(n.usage?.memory ?? "<unknown>"),
|
|
87
|
+
]);
|
|
88
|
+
console.log(renderTable(["NAME", "CPU", "MEMORY"], rows));
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const rows = items.map((p) => {
|
|
92
|
+
const containers = p.containers ?? [];
|
|
93
|
+
const cpu = containers.map((c) => `${c.name}:${c.usage?.cpu ?? "?"}`).join(",") || "<none>";
|
|
94
|
+
const memory = containers.map((c) => `${c.name}:${c.usage?.memory ?? "?"}`).join(",") || "<none>";
|
|
95
|
+
return [p.metadata?.namespace ?? "", p.metadata?.name ?? "", cpu, memory];
|
|
96
|
+
});
|
|
97
|
+
console.log(renderTable(["NAMESPACE", "NAME", "CPU (per container)", "MEMORY (per container)"], rows));
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chant verdict column `chant kube get` carries on every row (chant
|
|
3
|
+
* #1079's acceptance criterion: "get annotates each row with the declared /
|
|
4
|
+
* owned / drifted / foreign-owned verdict").
|
|
5
|
+
*
|
|
6
|
+
* Four states:
|
|
7
|
+
*
|
|
8
|
+
* - `declared` — this project's source names an entity at this exact
|
|
9
|
+
* apiVersion/kind/name(/namespace), and the live object agrees with it on
|
|
10
|
+
* every field the source declares.
|
|
11
|
+
* - `drifted` — same match, but the live object disagrees with the source on
|
|
12
|
+
* at least one declared field.
|
|
13
|
+
* - `owned` — no declared match, but the live object carries chant's
|
|
14
|
+
* ownership marker (`app.kubernetes.io/managed-by=chant`) — something
|
|
15
|
+
* chant applied that source no longer declares, or a stack/entity this
|
|
16
|
+
* project's build didn't resolve.
|
|
17
|
+
* - `foreign-owned` — no declared match and no marker: chant does not
|
|
18
|
+
* consider this object its own.
|
|
19
|
+
*
|
|
20
|
+
* `unavailable` is not a verdict about the object; it is what every row gets
|
|
21
|
+
* when the project context could not be loaded at all (chant #1079's
|
|
22
|
+
* "degrades gracefully outside a chant project" — `get` and `describe` still
|
|
23
|
+
* work, this column just has nothing to say).
|
|
24
|
+
*
|
|
25
|
+
* The drift check here is a fast, best-effort *declared-subset* comparison —
|
|
26
|
+
* every path the source sets must agree on the live object — not the
|
|
27
|
+
* managed-fields-precise diff `chant lifecycle diff --live --deep` computes
|
|
28
|
+
* (chant #1076). It exists for a table column that must render for dozens of
|
|
29
|
+
* rows in one `get` call; the deep, field-ownership-aware answer stays the
|
|
30
|
+
* lifecycle command's job. A live object can disagree with the source on a
|
|
31
|
+
* field a controller owns (Kubernetes rewriting `spec.replicas` under an
|
|
32
|
+
* HPA, say) and still show `drifted` here — reach for the deep diff before
|
|
33
|
+
* treating this column as a verdict rather than a hint.
|
|
34
|
+
*/
|
|
35
|
+
import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
36
|
+
function isPlainObject(v) {
|
|
37
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when every path `declared` sets is present and equal on `live`.
|
|
41
|
+
* Extra fields on `live` (server defaults, status, anything the source never
|
|
42
|
+
* mentions) are not drift — chant never declared an opinion about them.
|
|
43
|
+
*/
|
|
44
|
+
export function declaredSubsetMatches(declared, live) {
|
|
45
|
+
if (Array.isArray(declared)) {
|
|
46
|
+
if (!Array.isArray(live) || declared.length !== live.length)
|
|
47
|
+
return false;
|
|
48
|
+
return declared.every((item, i) => declaredSubsetMatches(item, live[i]));
|
|
49
|
+
}
|
|
50
|
+
if (isPlainObject(declared)) {
|
|
51
|
+
if (!isPlainObject(live))
|
|
52
|
+
return false;
|
|
53
|
+
return Object.entries(declared).every(([key, value]) => declaredSubsetMatches(value, live[key]));
|
|
54
|
+
}
|
|
55
|
+
return declared === live;
|
|
56
|
+
}
|
|
57
|
+
/** The declared fields worth comparing — everything but the name/namespace identity itself. */
|
|
58
|
+
function declaredComparable(props) {
|
|
59
|
+
if (!props)
|
|
60
|
+
return {};
|
|
61
|
+
const { metadata, ...rest } = props;
|
|
62
|
+
if (!isPlainObject(metadata))
|
|
63
|
+
return rest;
|
|
64
|
+
const { name: _name, namespace: _namespace, ...restMetadata } = metadata;
|
|
65
|
+
return { ...rest, ...(Object.keys(restMetadata).length > 0 ? { metadata: restMetadata } : {}) };
|
|
66
|
+
}
|
|
67
|
+
export function verdictFor(live, match) {
|
|
68
|
+
if (match) {
|
|
69
|
+
return declaredSubsetMatches(declaredComparable(match.props), live) ? "declared" : "drifted";
|
|
70
|
+
}
|
|
71
|
+
return hasOwnershipMarker(live.metadata?.labels, LABEL_OWNERSHIP_KEYS) ? "owned" : "foreign-owned";
|
|
72
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube wait` (chant #1079) — the issue names this one explicitly:
|
|
3
|
+
* "`wait` should use the existing readiness-spec registry in
|
|
4
|
+
* `op/activities/wait-for-ready.ts`, which already handles the Argo case
|
|
5
|
+
* where health and sync replace a `Ready` condition — something `kubectl
|
|
6
|
+
* wait --for` cannot express without jsonpath gymnastics." This verb is a
|
|
7
|
+
* thin CLI shell around that exact registry (`readinessFor`/`isReady`/
|
|
8
|
+
* `firstTerminal`/`waitForReady`) — not a second implementation of it.
|
|
9
|
+
*
|
|
10
|
+
* `--for=condition=<Type>[=<Status>]` overrides the registry with a single
|
|
11
|
+
* condition check, matching kubectl's own flag. `--for=delete` polls for
|
|
12
|
+
* absence instead. Neither given falls back to the registry: the generic
|
|
13
|
+
* `Ready` condition, or a resource-specific override (Argo's `Application`
|
|
14
|
+
* today).
|
|
15
|
+
*/
|
|
16
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
17
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
18
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
19
|
+
import { waitForReady, readinessFor, ReadinessFailedError, } from "../op/activities/wait-for-ready.js";
|
|
20
|
+
import { kubeConnect } from "./connect.js";
|
|
21
|
+
import { parseKubeFlags, connectOptionsFrom, parseDurationMs } from "./flags.js";
|
|
22
|
+
import { loadKubeProjectContext } from "./project.js";
|
|
23
|
+
import { resolveKubeTarget, isTargetError } from "./target.js";
|
|
24
|
+
function parseFor(value) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (value === "delete")
|
|
28
|
+
return { kind: "delete" };
|
|
29
|
+
const match = /^condition=([^=]+)(?:=(.+))?$/.exec(value);
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Error(`--for expects "condition=<Type>[=<Status>]" or "delete", got "${value}"`);
|
|
32
|
+
}
|
|
33
|
+
return { kind: "spec", spec: { ready: [{ conditionType: match[1], status: match[2] ?? "True" }] } };
|
|
34
|
+
}
|
|
35
|
+
function targetLabel(target) {
|
|
36
|
+
return "apiVersion" in target.selector ? target.selector.kind : target.selector.resource;
|
|
37
|
+
}
|
|
38
|
+
export async function runWait(rawArgs, deps = {}) {
|
|
39
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
40
|
+
const loadProject = deps.loadProject ?? loadKubeProjectContext;
|
|
41
|
+
let flags;
|
|
42
|
+
let forSpec;
|
|
43
|
+
let timeoutMs;
|
|
44
|
+
try {
|
|
45
|
+
flags = parseKubeFlags(rawArgs, { value: { "--for": "for", "--timeout": "timeout" } });
|
|
46
|
+
forSpec = parseFor(flags.values.for);
|
|
47
|
+
timeoutMs = flags.values.timeout !== undefined ? parseDurationMs(flags.values.timeout) : 30000;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const project = await loadProject();
|
|
54
|
+
const target = resolveKubeTarget(flags.positional, flags.values.namespace, project);
|
|
55
|
+
if (isTargetError(target)) {
|
|
56
|
+
console.error(`error: ${target.error}`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
if (!target.name) {
|
|
60
|
+
console.error("error: chant kube wait requires a single named resource, e.g. `chant kube wait deployment web`");
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
64
|
+
if (connected.kind === "unobserved") {
|
|
65
|
+
console.error(formatUnobserved(targetLabel(target), { reason: connected.reason, detail: connected.detail }));
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const { client } = connected;
|
|
69
|
+
let info;
|
|
70
|
+
try {
|
|
71
|
+
info = await client.resolve(target.selector);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const outcome = classifyApiFailure(err);
|
|
75
|
+
console.error(formatUnobserved(targetLabel(target), {
|
|
76
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
77
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
78
|
+
}));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (!info) {
|
|
82
|
+
console.error(`error: the server doesn't have a resource type "${targetLabel(target)}"`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
const namespace = target.namespace ?? client.defaultNamespace;
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
if (forSpec?.kind === "delete") {
|
|
90
|
+
await waitForDeletion(client, { apiVersion: info.apiVersion, kind: info.kind, name: target.name, namespace }, controller.signal);
|
|
91
|
+
console.log(`${info.kind}/${target.name} deleted`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
const spec = forSpec?.kind === "spec" ? forSpec.spec : readinessFor(info.group, info.kind);
|
|
95
|
+
const fetcher = async (args, signal) => (await client.read({ apiVersion: info.apiVersion, kind: info.kind, name: args.name, ...(args.namespace ? { namespace: args.namespace } : {}) }, { signal }));
|
|
96
|
+
await waitForReady({ kind: info.kind, name: target.name, namespace, group: info.group, spec }, controller.signal, fetcher);
|
|
97
|
+
console.log(`${info.kind}/${target.name} condition met`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof ReadinessFailedError) {
|
|
102
|
+
console.error(err.message);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
if (controller.signal.aborted) {
|
|
106
|
+
console.error(`error: timed out after ${timeoutMs}ms waiting for ${info.kind}/${target.name}`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const outcome = classifyApiFailure(err);
|
|
110
|
+
console.error(formatUnobserved(`${info.kind}/${target.name}`, {
|
|
111
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
112
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
113
|
+
}));
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function waitForDeletion(client, ref, signal, intervalMs = 5000) {
|
|
121
|
+
while (true) {
|
|
122
|
+
if (signal.aborted)
|
|
123
|
+
throw new Error("wait --for=delete aborted");
|
|
124
|
+
const obj = await client.readIfPresent(ref, { signal });
|
|
125
|
+
if (!obj)
|
|
126
|
+
return;
|
|
127
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kustomize renderer (#1548) — one implementation, two consumers.
|
|
3
|
+
*
|
|
4
|
+
* Piece 1 (`components/kustomize-apply.ts`) renders at DEPLOY time in front
|
|
5
|
+
* of the server-side applier; piece 3 (`./root.ts`) renders at BUILD time to
|
|
6
|
+
* give an overlay estate a declared side. Both run the same command with the
|
|
7
|
+
* same fallback through the same injectable runner, so a test asserts the
|
|
8
|
+
* exact invocation without either binary installed and the two paths cannot
|
|
9
|
+
* drift apart on how an overlay is rendered.
|
|
10
|
+
*
|
|
11
|
+
* Renderer resolution: `kustomize build`, falling back to `kubectl
|
|
12
|
+
* kustomize` (the same renderer vendored into kubectl) when the standalone
|
|
13
|
+
* binary is absent. When NEITHER binary exists the failure names both — a
|
|
14
|
+
* missing renderer is an environment problem the operator can fix in one
|
|
15
|
+
* install, and it must read as that, not as a stack trace. A render failure
|
|
16
|
+
* from an existing binary (a broken overlay, a missing base) propagates
|
|
17
|
+
* untouched: that error text is kustomize's own diagnosis and is strictly
|
|
18
|
+
* better than anything this module could wrap it in.
|
|
19
|
+
*
|
|
20
|
+
* The 64MiB maxBuffer mirrors helm-upgrade's — a big overlay renders
|
|
21
|
+
* megabytes.
|
|
22
|
+
*/
|
|
23
|
+
import { exec } from "node:child_process";
|
|
24
|
+
import { promisify } from "node:util";
|
|
25
|
+
import { loadAll } from "js-yaml";
|
|
26
|
+
const execAsync = promisify(exec);
|
|
27
|
+
export const defaultKustomizeRunner = (command) => execAsync(command, { maxBuffer: 64 * 1024 * 1024 });
|
|
28
|
+
/** Single-quote shell escaping, as helm-upgrade quotes its argv. */
|
|
29
|
+
function q(v) {
|
|
30
|
+
return `'${v.replace(/'/g, "'\\''")}'`;
|
|
31
|
+
}
|
|
32
|
+
/** The exact render command, standalone binary first. Pure; exported for tests. */
|
|
33
|
+
export function renderCommand(dir, tool = "kustomize") {
|
|
34
|
+
return tool === "kustomize" ? `kustomize build ${q(dir)}` : `kubectl kustomize ${q(dir)}`;
|
|
35
|
+
}
|
|
36
|
+
/** Does this error mean "the binary is not installed" (vs "the render failed")? */
|
|
37
|
+
function isMissingBinary(err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return /ENOENT|not found|command not found|127/.test(message);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Render the kustomization at `dir` and parse the emitted YAML into document
|
|
43
|
+
* objects. Falls back to kubectl's vendored kustomize when the standalone
|
|
44
|
+
* binary is missing; when both binaries are missing, fails with a message
|
|
45
|
+
* naming them.
|
|
46
|
+
*/
|
|
47
|
+
export async function renderKustomizeDocuments(dir, run = defaultKustomizeRunner) {
|
|
48
|
+
let stdout;
|
|
49
|
+
try {
|
|
50
|
+
({ stdout } = await run(renderCommand(dir, "kustomize")));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (!isMissingBinary(err))
|
|
54
|
+
throw err;
|
|
55
|
+
try {
|
|
56
|
+
({ stdout } = await run(renderCommand(dir, "kubectl")));
|
|
57
|
+
}
|
|
58
|
+
catch (err2) {
|
|
59
|
+
if (isMissingBinary(err2)) {
|
|
60
|
+
throw new Error(`Cannot render kustomization "${dir}": neither the \`kustomize\` binary nor \`kubectl\` ` +
|
|
61
|
+
`(whose vendored \`kubectl kustomize\` is the fallback) was found on PATH. ` +
|
|
62
|
+
`Install kustomize or kubectl and retry.`);
|
|
63
|
+
}
|
|
64
|
+
throw err2;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const documents = [];
|
|
68
|
+
for (const doc of loadAll(stdout)) {
|
|
69
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
70
|
+
documents.push(doc);
|
|
71
|
+
}
|
|
72
|
+
return documents;
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rendered kustomize document as a chant entity (#1548 piece 3).
|
|
3
|
+
*
|
|
4
|
+
* A kustomize build root's output is a set of finished manifests, not typed
|
|
5
|
+
* chant source — the overlay already decided every field. The entity that
|
|
6
|
+
* carries one into the build is therefore the verbatim manifest entity
|
|
7
|
+
* (`../manifest-entity`), shared since #999 with `chant carve emit`'s adoption
|
|
8
|
+
* of a Terraform `kubernetes_manifest`: `props` IS the document, and the
|
|
9
|
+
* serializer emits it as-is.
|
|
10
|
+
*
|
|
11
|
+
* Provenance: every rendered document is stamped with an annotation naming
|
|
12
|
+
* the overlay dir it came from, so a consumer (behold#171's overlay boxes)
|
|
13
|
+
* can group by declared origin instead of guessing from paths — the one thing
|
|
14
|
+
* this adds over a plain manifest entity.
|
|
15
|
+
*/
|
|
16
|
+
import { manifestEntity, RENDERED_MANIFEST_MARKER, isRenderedManifestEntity } from "../manifest-entity.js";
|
|
17
|
+
export { RENDERED_MANIFEST_MARKER, isRenderedManifestEntity };
|
|
18
|
+
/** Annotation stamped on every rendered document, valued with the root dir. */
|
|
19
|
+
export const KUSTOMIZE_ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
|
|
20
|
+
/**
|
|
21
|
+
* Wrap one rendered document. Returns null when the document has no string
|
|
22
|
+
* `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
|
|
23
|
+
*
|
|
24
|
+
* `annotation` is the provenance key: the kustomize root annotation by
|
|
25
|
+
* default, `k8sInclude`'s source annotation for an included file (#3566).
|
|
26
|
+
*/
|
|
27
|
+
export function renderedManifestEntity(doc, root, annotation = KUSTOMIZE_ROOT_ANNOTATION) {
|
|
28
|
+
const metadata = { ...(doc.metadata ?? {}) };
|
|
29
|
+
metadata.annotations = {
|
|
30
|
+
...(metadata.annotations ?? {}),
|
|
31
|
+
[annotation]: root,
|
|
32
|
+
};
|
|
33
|
+
return manifestEntity({ ...doc, metadata });
|
|
34
|
+
}
|
|
35
|
+
/** `Deployment` + `my-app` → `deploymentMyApp`, the import path's naming. */
|
|
36
|
+
export function renderedLogicalId(kind, name) {
|
|
37
|
+
const prefix = kind.charAt(0).toLowerCase() + kind.slice(1);
|
|
38
|
+
if (!name)
|
|
39
|
+
return prefix;
|
|
40
|
+
const pascal = name
|
|
41
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
44
|
+
.join("");
|
|
45
|
+
return `${prefix}${pascal}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Add a rendered entity under a deterministic, human-readable key:
|
|
49
|
+
* `<prefix>/<kindName>`. Two documents can share kind+name (different
|
|
50
|
+
* namespaces, or different API groups with the same kind word), so a
|
|
51
|
+
* collision is qualified by namespace first, then numbered in document
|
|
52
|
+
* order. Returns the key used.
|
|
53
|
+
*/
|
|
54
|
+
export function addRenderedEntity(entities, prefix, entity) {
|
|
55
|
+
const metadata = entity.props.metadata;
|
|
56
|
+
const kind = entity.props.kind;
|
|
57
|
+
const name = typeof metadata.name === "string" ? metadata.name : undefined;
|
|
58
|
+
const namespace = typeof metadata.namespace === "string" ? metadata.namespace : undefined;
|
|
59
|
+
const base = `${prefix}/${renderedLogicalId(kind, name)}`;
|
|
60
|
+
let key = base;
|
|
61
|
+
if (entities.has(key) && namespace)
|
|
62
|
+
key = `${base}.${namespace}`;
|
|
63
|
+
for (let n = 2; entities.has(key); n++)
|
|
64
|
+
key = `${base}~${n}`;
|
|
65
|
+
entities.set(key, entity);
|
|
66
|
+
return key;
|
|
67
|
+
}
|