@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube apply` (chant #1079) — "writes are not cloned": this calls
|
|
3
|
+
* `applyManifest` (`../op/activities/kubectl.ts`) directly, the exact
|
|
4
|
+
* function the k8s Op activity registers under `kubectlApply` — server-side
|
|
5
|
+
* apply, chant's field-manager identity, the same `FieldManagerConflictError`
|
|
6
|
+
* surface (chant #1075/#1178) — never a second implementation of `kubectl
|
|
7
|
+
* apply`. An explicit manifest path is mandatory; there is no bare-sweep form.
|
|
8
|
+
*
|
|
9
|
+
* The one thing this verb adds on top of the activity is a confirmation
|
|
10
|
+
* gate, matching the issue's "route through plan, gate, and ownership": a
|
|
11
|
+
* bare `chant kube apply -f x.yaml` performs a server-side dry run — every
|
|
12
|
+
* document validated and previewed, nothing persisted — and says so; only
|
|
13
|
+
* `--yes` performs the real, persisting apply. That is a deliberate
|
|
14
|
+
* divergence from `kubectl apply`, which persists by default; a terminal
|
|
15
|
+
* command that can rewrite a live Deployment is worth one extra keystroke of
|
|
16
|
+
* intent.
|
|
17
|
+
*/
|
|
18
|
+
import { ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
|
|
19
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
20
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
21
|
+
import { classifyApiFailure, isMissingClientPackage, MISSING_CLIENT_DETAIL } from "../api/classify.js";
|
|
22
|
+
import { applyManifest } from "../op/activities/kubectl.js";
|
|
23
|
+
import { parseKubeFlags } from "./flags.js";
|
|
24
|
+
export async function runApply(rawArgs, deps = {}) {
|
|
25
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
26
|
+
const apply = deps.apply ?? applyManifest;
|
|
27
|
+
let flags;
|
|
28
|
+
try {
|
|
29
|
+
flags = parseKubeFlags(rawArgs, {
|
|
30
|
+
value: { "-f": "filename", "--filename": "filename", "--field-manager": "fieldManager" },
|
|
31
|
+
boolean: { "--force-conflicts": "forceConflicts", "--yes": "yes", "-y": "yes" },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const manifest = flags.values.filename;
|
|
39
|
+
if (!manifest) {
|
|
40
|
+
console.error("error: --filename/-f is required — chant kube apply never applies without an explicit manifest path (no bare sweeps)");
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const dryRun = !(flags.flags.yes ?? false);
|
|
44
|
+
const args = {
|
|
45
|
+
manifest,
|
|
46
|
+
...(flags.values.env !== undefined ? { environment: flags.values.env } : {}),
|
|
47
|
+
...(flags.values.context !== undefined ? { context: flags.values.context } : {}),
|
|
48
|
+
...(flags.values.fieldManager !== undefined ? { fieldManager: flags.values.fieldManager } : {}),
|
|
49
|
+
force: flags.flags.forceConflicts ?? false,
|
|
50
|
+
dryRun,
|
|
51
|
+
};
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await apply(args, undefined, connect);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof Error && err.name === "FieldManagerConflictError") {
|
|
58
|
+
console.error(err.message);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (err instanceof ClusterBindingMismatchError) {
|
|
62
|
+
console.error(formatUnobserved(manifest, { reason: "no-binding", detail: err.message }));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if (isMissingClientPackage(err)) {
|
|
66
|
+
console.error(formatUnobserved(manifest, { reason: "read-failed", detail: MISSING_CLIENT_DETAIL }));
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const outcome = classifyApiFailure(err);
|
|
70
|
+
console.error(formatUnobserved(manifest, {
|
|
71
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
72
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
73
|
+
}));
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
// applyManifest already logged one line per applied object (including,
|
|
77
|
+
// since chant #1079, whether it was a dry run) — this is the summary.
|
|
78
|
+
if (dryRun) {
|
|
79
|
+
console.log(`DRY RUN — ${result.applied.length} object(s) server-validated, nothing persisted. Pass --yes to apply for real.`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`applied ${result.applied.length} object(s) as field manager "${result.fieldManager}"`);
|
|
83
|
+
if (result.pruned.length > 0)
|
|
84
|
+
console.log(`pruned ${result.pruned.length} chant-owned object(s) no longer declared`);
|
|
85
|
+
if ((result.retained ?? []).length > 0) {
|
|
86
|
+
console.log(`retained ${result.retained.length} generated-once secret(s) no longer declared — never pruned; delete explicitly if you mean to`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connecting to a cluster on `chant kube`'s behalf (chant #1079), honoring
|
|
3
|
+
* the exact same environment→cluster binding every other observing path
|
|
4
|
+
* does (chant #1100/#1155, revised by #1488): `--env` resolves
|
|
5
|
+
* `k8s.profiles.<env>.context` and uses it regardless of the ambient
|
|
6
|
+
* context, `--context` is an explicit override that skips the binding
|
|
7
|
+
* entirely, and neither given falls back to the ambient kubeconfig
|
|
8
|
+
* context — the same three modes `describeResources` and the Op
|
|
9
|
+
* activities already use, via the same `defaultK8sConnector`. Nothing here
|
|
10
|
+
* reimplements that resolution; this only turns a connect failure into the
|
|
11
|
+
* observation tri-state so a verb can render NOT-OBSERVED-with-reason
|
|
12
|
+
* instead of a stack trace.
|
|
13
|
+
*/
|
|
14
|
+
import { ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
|
|
15
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
16
|
+
import { classifyApiFailure, isMissingClientPackage, isWholeLexiconFailure, MISSING_CLIENT_DETAIL } from "../api/classify.js";
|
|
17
|
+
/**
|
|
18
|
+
* Connect, or classify the failure into the tri-state. A bound-but-mismatched
|
|
19
|
+
* `--env` (`ClusterBindingMismatchError`), a missing `@intentius/chant-k8s-client`
|
|
20
|
+
* install, and a refused exec-credential plugin are the "nothing left the
|
|
21
|
+
* process" failures every verb must treat identically: NOT-OBSERVED, never
|
|
22
|
+
* an empty/absent result.
|
|
23
|
+
*/
|
|
24
|
+
export async function kubeConnect(options, connect = defaultK8sConnector) {
|
|
25
|
+
try {
|
|
26
|
+
const connected = await connect(options);
|
|
27
|
+
return { kind: "connected", ...connected };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (isMissingClientPackage(err)) {
|
|
31
|
+
return { kind: "unobserved", reason: "read-failed", detail: MISSING_CLIENT_DETAIL };
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof ClusterBindingMismatchError) {
|
|
34
|
+
return { kind: "unobserved", reason: "no-binding", detail: err.message };
|
|
35
|
+
}
|
|
36
|
+
if (isWholeLexiconFailure(err)) {
|
|
37
|
+
const outcome = classifyApiFailure(err);
|
|
38
|
+
return {
|
|
39
|
+
kind: "unobserved",
|
|
40
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
41
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube delete` (chant #1079) — routes through the typed client's own
|
|
3
|
+
* `delete()` (`packages/k8s-client/src/client.ts`), the identical typed
|
|
4
|
+
* delete `applyManifest`'s prune step already uses (chant #1075) — never a
|
|
5
|
+
* second implementation. A kind and a name are both mandatory: there is no
|
|
6
|
+
* bare `chant kube delete deployments` form, so nothing here can become a
|
|
7
|
+
* sweep by omission (the issue's "nothing destructive happens without an
|
|
8
|
+
* explicit resource argument").
|
|
9
|
+
*
|
|
10
|
+
* Two guardrails on top of the typed delete itself:
|
|
11
|
+
*
|
|
12
|
+
* - **Gate.** A bare invocation reads the object, reports what it found (and
|
|
13
|
+
* whether chant owns it), and stops — only `--yes` performs the delete.
|
|
14
|
+
* - **Ownership, surfaced not enforced.** kubectl itself deletes anything
|
|
15
|
+
* RBAC allows, chant-applied or not, and this keeps that: an explicit name
|
|
16
|
+
* is already an informed decision. What's added is visibility — a
|
|
17
|
+
* warning when the object carries no chant ownership marker, so the
|
|
18
|
+
* operator knows they are about to delete something chant did not apply.
|
|
19
|
+
*/
|
|
20
|
+
import { ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
|
|
21
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
22
|
+
import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
23
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
24
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
25
|
+
import { kubeConnect } from "./connect.js";
|
|
26
|
+
import { parseKubeFlags, connectOptionsFrom } from "./flags.js";
|
|
27
|
+
import { loadKubeProjectContext } from "./project.js";
|
|
28
|
+
import { resolveKubeTarget, isTargetError } from "./target.js";
|
|
29
|
+
function targetLabel(target) {
|
|
30
|
+
return "apiVersion" in target.selector ? target.selector.kind : target.selector.resource;
|
|
31
|
+
}
|
|
32
|
+
const PROPAGATION_POLICIES = new Set(["Foreground", "Background", "Orphan"]);
|
|
33
|
+
export async function runDelete(rawArgs, deps = {}) {
|
|
34
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
35
|
+
const loadProject = deps.loadProject ?? loadKubeProjectContext;
|
|
36
|
+
let flags;
|
|
37
|
+
try {
|
|
38
|
+
flags = parseKubeFlags(rawArgs, {
|
|
39
|
+
value: { "--propagation-policy": "propagationPolicy" },
|
|
40
|
+
boolean: { "--yes": "yes", "-y": "yes" },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (flags.values.propagationPolicy !== undefined && !PROPAGATION_POLICIES.has(flags.values.propagationPolicy)) {
|
|
48
|
+
console.error(`error: --propagation-policy must be one of Foreground, Background, Orphan`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const project = await loadProject();
|
|
52
|
+
const target = resolveKubeTarget(flags.positional, flags.values.namespace, project);
|
|
53
|
+
if (isTargetError(target)) {
|
|
54
|
+
console.error(`error: ${target.error}`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
if (!target.name) {
|
|
58
|
+
console.error("error: chant kube delete requires an explicit resource name — no bare kind sweeps " +
|
|
59
|
+
"(e.g. `chant kube delete deployment web`, not `chant kube delete deployment`)");
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
63
|
+
if (connected.kind === "unobserved") {
|
|
64
|
+
console.error(formatUnobserved(targetLabel(target), { reason: connected.reason, detail: connected.detail }));
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const { client } = connected;
|
|
68
|
+
let info;
|
|
69
|
+
try {
|
|
70
|
+
info = await client.resolve(target.selector);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const outcome = classifyApiFailure(err);
|
|
74
|
+
console.error(formatUnobserved(targetLabel(target), {
|
|
75
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
76
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
77
|
+
}));
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (!info) {
|
|
81
|
+
console.error(`error: the server doesn't have a resource type "${targetLabel(target)}"`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const namespace = target.namespace ?? client.defaultNamespace;
|
|
85
|
+
const ref = { apiVersion: info.apiVersion, kind: info.kind, name: target.name, namespace };
|
|
86
|
+
const object = await readForDelete(client, ref);
|
|
87
|
+
if (object.kind === "unobserved") {
|
|
88
|
+
console.error(formatUnobserved(`${info.kind}/${target.name}`, { reason: object.reason, detail: object.detail }));
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (!object.found) {
|
|
92
|
+
console.error(`Error from server (NotFound): ${info.name} "${target.name}" not found`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const owned = hasOwnershipMarker(object.labels, LABEL_OWNERSHIP_KEYS);
|
|
96
|
+
const label = `${info.kind}/${target.name}${namespace ? ` -n ${namespace}` : ""}`;
|
|
97
|
+
if (!(flags.flags.yes ?? false)) {
|
|
98
|
+
console.log(`Would delete ${label}${owned ? "" : " (warning: not chant-owned)"}. Pass --yes to delete for real.`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (!owned) {
|
|
102
|
+
console.log(`warning: ${label} carries no chant ownership marker — deleting an object chant did not apply`);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await client.delete({ apiVersion: info.apiVersion, kind: info.kind, name: target.name, ...(namespace ? { namespace } : {}) }, { ...(flags.values.propagationPolicy ? { propagationPolicy: flags.values.propagationPolicy } : {}) });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const outcome = classifyApiFailure(err);
|
|
109
|
+
if (outcome.kind === "absent") {
|
|
110
|
+
console.error(`Error from server (NotFound): ${info.name} "${target.name}" not found`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
console.error(formatUnobserved(label, {
|
|
114
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
115
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
116
|
+
}));
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
console.log(`${label} deleted`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
async function readForDelete(client, ref) {
|
|
123
|
+
try {
|
|
124
|
+
const obj = await client.readIfPresent(ref);
|
|
125
|
+
return obj ? { kind: "found", found: true, labels: obj.metadata?.labels } : { kind: "found", found: false, labels: undefined };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof ClusterBindingMismatchError) {
|
|
129
|
+
return { kind: "unobserved", reason: "no-binding", detail: err.message };
|
|
130
|
+
}
|
|
131
|
+
const outcome = classifyApiFailure(err);
|
|
132
|
+
return {
|
|
133
|
+
kind: "unobserved",
|
|
134
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
135
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube describe` (chant #1079) — a generic, multi-section human view:
|
|
3
|
+
* identity, chant's own verdict + provenance (when inside a project), a
|
|
4
|
+
* pretty-printed spec/status, and the object's Events (reusing `./events.ts`'s
|
|
5
|
+
* `listEvents`/`filterInvolved`, the same machinery `chant kube events`
|
|
6
|
+
* itself uses — one implementation, not two). This is not kubectl's
|
|
7
|
+
* per-kind template set (`OldReplicaSets`/`NewReplicaSet` for a Deployment,
|
|
8
|
+
* per-container detail for a Pod, and so on for every built-in kind) — a
|
|
9
|
+
* generic describe covering the fields every object carries, plus the
|
|
10
|
+
* context only chant can add.
|
|
11
|
+
*/
|
|
12
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
13
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
14
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
15
|
+
import { statusFromObject } from "../describe-resources.js";
|
|
16
|
+
import { kubeConnect } from "./connect.js";
|
|
17
|
+
import { parseKubeFlags, connectOptionsFrom } from "./flags.js";
|
|
18
|
+
import { loadKubeProjectContext, relativeProvenance } from "./project.js";
|
|
19
|
+
import { resolveKubeTarget, matchLiveObject, isTargetError } from "./target.js";
|
|
20
|
+
import { verdictFor } from "./verdict.js";
|
|
21
|
+
import { relativeAge } from "./render.js";
|
|
22
|
+
import { listEvents, filterInvolved, renderEvents } from "./events.js";
|
|
23
|
+
function targetLabel(target) {
|
|
24
|
+
return "apiVersion" in target.selector ? target.selector.kind : target.selector.resource;
|
|
25
|
+
}
|
|
26
|
+
function keyValueBlock(m) {
|
|
27
|
+
if (!m || Object.keys(m).length === 0)
|
|
28
|
+
return "<none>";
|
|
29
|
+
const entries = Object.entries(m);
|
|
30
|
+
return entries.map(([k, v], i) => (i === 0 ? `${k}=${v}` : ` ${k}=${v}`)).join("\n");
|
|
31
|
+
}
|
|
32
|
+
function indentedJson(value) {
|
|
33
|
+
return JSON.stringify(value, null, 2)
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((l) => ` ${l}`)
|
|
36
|
+
.join("\n");
|
|
37
|
+
}
|
|
38
|
+
async function renderBlock(client, obj, info, project) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push(`Name: ${obj.metadata?.name ?? ""}`);
|
|
41
|
+
if (obj.metadata?.namespace)
|
|
42
|
+
lines.push(`Namespace: ${obj.metadata.namespace}`);
|
|
43
|
+
lines.push(`API Version: ${obj.apiVersion ?? info.apiVersion}`);
|
|
44
|
+
lines.push(`Kind: ${obj.kind ?? info.kind}`);
|
|
45
|
+
lines.push(`Labels: ${keyValueBlock(obj.metadata?.labels)}`);
|
|
46
|
+
lines.push(`Annotations: ${keyValueBlock(obj.metadata?.annotations)}`);
|
|
47
|
+
lines.push(`Created: ${obj.metadata?.creationTimestamp ?? "<unknown>"} (${relativeAge(obj.metadata?.creationTimestamp)} ago)`);
|
|
48
|
+
lines.push(`Status: ${statusFromObject(obj)}`);
|
|
49
|
+
lines.push("Chant:");
|
|
50
|
+
if (!project) {
|
|
51
|
+
lines.push(" Verdict: unavailable (not running inside a chant project)");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const match = matchLiveObject(project, {
|
|
55
|
+
apiVersion: obj.apiVersion ?? info.apiVersion,
|
|
56
|
+
kind: obj.kind ?? info.kind,
|
|
57
|
+
name: obj.metadata?.name ?? "",
|
|
58
|
+
namespace: obj.metadata?.namespace,
|
|
59
|
+
});
|
|
60
|
+
lines.push(` Verdict: ${verdictFor(obj, match)}`);
|
|
61
|
+
if (match) {
|
|
62
|
+
const prov = relativeProvenance(project, match.entity);
|
|
63
|
+
lines.push(` Entity: ${match.entityName}`);
|
|
64
|
+
lines.push(` Source: ${prov?.sourceFile ?? "(unknown)"}${prov?.composite ? ` (via ${prov.composite})` : ""}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (obj.spec !== undefined) {
|
|
68
|
+
lines.push("Spec:");
|
|
69
|
+
lines.push(indentedJson(obj.spec));
|
|
70
|
+
}
|
|
71
|
+
if (obj.status !== undefined) {
|
|
72
|
+
lines.push("Status:");
|
|
73
|
+
lines.push(indentedJson(obj.status));
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const events = await listEvents(client, { namespace: obj.metadata?.namespace });
|
|
77
|
+
const involved = filterInvolved(events, { kind: obj.kind ?? info.kind, name: obj.metadata?.name ?? "" });
|
|
78
|
+
lines.push("Events:");
|
|
79
|
+
lines.push(renderEvents(involved));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
lines.push("Events: <could not be read>");
|
|
83
|
+
}
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
export async function runDescribe(rawArgs, deps = {}) {
|
|
87
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
88
|
+
const loadProject = deps.loadProject ?? loadKubeProjectContext;
|
|
89
|
+
let flags;
|
|
90
|
+
try {
|
|
91
|
+
flags = parseKubeFlags(rawArgs);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const project = await loadProject();
|
|
98
|
+
const target = resolveKubeTarget(flags.positional, flags.values.namespace, project);
|
|
99
|
+
if (isTargetError(target)) {
|
|
100
|
+
console.error(`error: ${target.error}`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
104
|
+
if (connected.kind === "unobserved") {
|
|
105
|
+
console.error(formatUnobserved(targetLabel(target), { reason: connected.reason, detail: connected.detail }));
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const { client } = connected;
|
|
109
|
+
let info;
|
|
110
|
+
try {
|
|
111
|
+
info = await client.resolve(target.selector);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const outcome = classifyApiFailure(err);
|
|
115
|
+
console.error(formatUnobserved(targetLabel(target), {
|
|
116
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
117
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
118
|
+
}));
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
if (!info) {
|
|
122
|
+
console.error(`error: the server doesn't have a resource type "${targetLabel(target)}"`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
const namespace = flags.flags.allNamespaces ? undefined : (target.namespace ?? client.defaultNamespace);
|
|
126
|
+
const ref = { apiVersion: info.apiVersion, kind: info.kind };
|
|
127
|
+
let objects;
|
|
128
|
+
if (target.name) {
|
|
129
|
+
try {
|
|
130
|
+
const obj = await client.readIfPresent({ ...ref, name: target.name, ...(namespace ? { namespace } : {}) });
|
|
131
|
+
if (!obj) {
|
|
132
|
+
console.error(`Error from server (NotFound): ${info.name} "${target.name}" not found`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
objects = [obj];
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const outcome = classifyApiFailure(err);
|
|
139
|
+
console.error(formatUnobserved(`${info.kind}/${target.name}`, {
|
|
140
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
141
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
142
|
+
}));
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
try {
|
|
148
|
+
objects = await client.list(ref, {
|
|
149
|
+
...(namespace ? { namespace } : {}),
|
|
150
|
+
...(flags.values.selector ? { labelSelector: flags.values.selector } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const outcome = classifyApiFailure(err);
|
|
155
|
+
console.error(formatUnobserved(info.kind, {
|
|
156
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
157
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
158
|
+
}));
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (objects.length === 0) {
|
|
163
|
+
console.log("No resources found.");
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
const blocks = await Promise.all(objects.map((obj) => renderBlock(client, obj, info, project)));
|
|
167
|
+
console.log(blocks.join("\n\n\n"));
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant kube events` (chant #1079) — the core `v1 Event` kind is addressed
|
|
3
|
+
* exactly like any other read (`client.list`), no client extension needed.
|
|
4
|
+
* The one thing the typed client's `ListOptions` does not carry is a field
|
|
5
|
+
* selector, so `--for <kind>/<name>` (kubectl's own `kubectl events
|
|
6
|
+
* --for=kind/name` shape) filters client-side against `involvedObject` after
|
|
7
|
+
* the list comes back, rather than asking the server to. Event volume in a
|
|
8
|
+
* single namespace is small enough that this is a fine trade for not
|
|
9
|
+
* widening the client's surface for one caller.
|
|
10
|
+
*/
|
|
11
|
+
import { formatUnobserved } from "@intentius/chant/observation";
|
|
12
|
+
import { defaultK8sConnector } from "../api/connect.js";
|
|
13
|
+
import { classifyApiFailure } from "../api/classify.js";
|
|
14
|
+
import { kubeConnect } from "./connect.js";
|
|
15
|
+
import { parseKubeFlags, connectOptionsFrom } from "./flags.js";
|
|
16
|
+
import { renderTable, renderJson, renderYaml, relativeAge, parseOutput } from "./render.js";
|
|
17
|
+
function involvedOf(event) {
|
|
18
|
+
return event.involvedObject ?? {};
|
|
19
|
+
}
|
|
20
|
+
function eventTimestamp(event) {
|
|
21
|
+
return (event.lastTimestamp ??
|
|
22
|
+
event.eventTime ??
|
|
23
|
+
event.metadata?.creationTimestamp);
|
|
24
|
+
}
|
|
25
|
+
/** Events whose `involvedObject` names `ref`, oldest first — kubectl's own default order. */
|
|
26
|
+
export function filterInvolved(events, ref) {
|
|
27
|
+
return events.filter((e) => {
|
|
28
|
+
const involved = involvedOf(e);
|
|
29
|
+
return involved.kind === ref.kind && involved.name === ref.name;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function sortByTime(events) {
|
|
33
|
+
return [...events].sort((a, b) => {
|
|
34
|
+
const ta = eventTimestamp(a);
|
|
35
|
+
const tb = eventTimestamp(b);
|
|
36
|
+
if (!ta && !tb)
|
|
37
|
+
return 0;
|
|
38
|
+
if (!ta)
|
|
39
|
+
return -1;
|
|
40
|
+
if (!tb)
|
|
41
|
+
return 1;
|
|
42
|
+
return Date.parse(ta) - Date.parse(tb);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* List Events for `namespace` (or every namespace, when `namespace` is
|
|
47
|
+
* undefined), sorted oldest first. Shared by `runEvents` and `describe`'s own
|
|
48
|
+
* Events section, so the two verbs agree on what "the events for this object"
|
|
49
|
+
* means.
|
|
50
|
+
*/
|
|
51
|
+
export async function listEvents(client, options = {}) {
|
|
52
|
+
const events = await client.list({ apiVersion: "v1", kind: "Event" }, { ...(options.namespace ? { namespace: options.namespace } : {}), ...(options.labelSelector ? { labelSelector: options.labelSelector } : {}) });
|
|
53
|
+
return sortByTime(events);
|
|
54
|
+
}
|
|
55
|
+
function eventRow(e) {
|
|
56
|
+
const involved = involvedOf(e);
|
|
57
|
+
const count = typeof e.count === "number" ? e.count : undefined;
|
|
58
|
+
return [
|
|
59
|
+
relativeAge(eventTimestamp(e)),
|
|
60
|
+
e.type ?? "",
|
|
61
|
+
e.reason ?? "",
|
|
62
|
+
`${involved.kind ?? ""}/${involved.name ?? ""}`,
|
|
63
|
+
`${e.message ?? ""}${count && count > 1 ? ` (x${count})` : ""}`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/** Render an events table — reused by `describe`'s Events section. */
|
|
67
|
+
export function renderEvents(events) {
|
|
68
|
+
if (events.length === 0)
|
|
69
|
+
return "No events found.";
|
|
70
|
+
return renderTable(["LAST SEEN", "TYPE", "REASON", "OBJECT", "MESSAGE"], events.map(eventRow));
|
|
71
|
+
}
|
|
72
|
+
function parseFor(value) {
|
|
73
|
+
if (!value)
|
|
74
|
+
return undefined;
|
|
75
|
+
const slash = value.indexOf("/");
|
|
76
|
+
if (slash === -1)
|
|
77
|
+
throw new Error(`--for expects KIND/NAME, e.g. --for=Pod/web-abc123`);
|
|
78
|
+
return { kind: value.slice(0, slash), name: value.slice(slash + 1) };
|
|
79
|
+
}
|
|
80
|
+
export async function runEvents(rawArgs, deps = {}) {
|
|
81
|
+
const connect = deps.connect ?? defaultK8sConnector;
|
|
82
|
+
let flags;
|
|
83
|
+
try {
|
|
84
|
+
flags = parseKubeFlags(rawArgs, { value: { "--for": "for" } });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
let output;
|
|
91
|
+
let forRef;
|
|
92
|
+
try {
|
|
93
|
+
output = parseOutput(flags.values.output);
|
|
94
|
+
forRef = parseFor(flags.values.for);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
|
|
101
|
+
if (connected.kind === "unobserved") {
|
|
102
|
+
console.error(formatUnobserved("events", { reason: connected.reason, detail: connected.detail }));
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const { client } = connected;
|
|
106
|
+
const namespace = flags.flags.allNamespaces ? undefined : (flags.values.namespace ?? client.defaultNamespace);
|
|
107
|
+
let events;
|
|
108
|
+
try {
|
|
109
|
+
events = await listEvents(client, { namespace, ...(flags.values.selector ? { labelSelector: flags.values.selector } : {}) });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const outcome = classifyApiFailure(err);
|
|
113
|
+
console.error(formatUnobserved("events", {
|
|
114
|
+
reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
115
|
+
detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
|
|
116
|
+
}));
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
if (forRef)
|
|
120
|
+
events = filterInvolved(events, forRef);
|
|
121
|
+
switch (output.kind) {
|
|
122
|
+
case "json":
|
|
123
|
+
console.log(renderJson(events));
|
|
124
|
+
return 0;
|
|
125
|
+
case "yaml":
|
|
126
|
+
console.log(renderYaml(events));
|
|
127
|
+
return 0;
|
|
128
|
+
default:
|
|
129
|
+
console.log(renderEvents(events));
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag parsing shared by every `chant kube <verb>` (chant #1079).
|
|
3
|
+
*
|
|
4
|
+
* `CommandGroupContext.rawArgs` is unparsed argv — core has no vocabulary for
|
|
5
|
+
* a lexicon's own verbs (chant #1078) — so every verb here does its own
|
|
6
|
+
* parsing, reusing `splitJoinedFlags`/`unknownFlagError` for the same
|
|
7
|
+
* `--flag=value` splitting and rejection discipline core's own parser
|
|
8
|
+
* applies (chant #1127). This module adds the flag vocabulary every verb
|
|
9
|
+
* shares (namespace, selector, output, cluster targeting) so each verb file
|
|
10
|
+
* only has to declare what is actually its own (`--tail`, `--for`, ...).
|
|
11
|
+
*/
|
|
12
|
+
import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
|
|
13
|
+
/** Every `chant kube` verb accepts these — kubectl's own names. */
|
|
14
|
+
const COMMON_VALUE = {
|
|
15
|
+
"-n": "namespace",
|
|
16
|
+
"--namespace": "namespace",
|
|
17
|
+
"-l": "selector",
|
|
18
|
+
"--selector": "selector",
|
|
19
|
+
"-o": "output",
|
|
20
|
+
"--output": "output",
|
|
21
|
+
"--context": "context",
|
|
22
|
+
"--env": "env",
|
|
23
|
+
"--kubeconfig": "kubeconfig",
|
|
24
|
+
};
|
|
25
|
+
const COMMON_BOOLEAN = {
|
|
26
|
+
"-A": "allNamespaces",
|
|
27
|
+
"--all-namespaces": "allNamespaces",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Parse `rawArgs` against the common flag set plus `spec`. Throws
|
|
31
|
+
* {@link unknownFlagError}'s shape on anything starting with `-` that
|
|
32
|
+
* neither set recognizes, and on a value flag given with nothing after it.
|
|
33
|
+
*/
|
|
34
|
+
export function parseKubeFlags(rawArgs, spec = {}) {
|
|
35
|
+
const valueFlags = { ...COMMON_VALUE, ...(spec.value ?? {}) };
|
|
36
|
+
const booleanFlags = { ...COMMON_BOOLEAN, ...(spec.boolean ?? {}) };
|
|
37
|
+
const args = splitJoinedFlags(rawArgs, new Set(Object.keys(booleanFlags)));
|
|
38
|
+
const positional = [];
|
|
39
|
+
const values = {};
|
|
40
|
+
const flags = {};
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (!arg.startsWith("-") || arg === "-") {
|
|
44
|
+
positional.push(arg);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg in booleanFlags) {
|
|
48
|
+
flags[booleanFlags[arg]] = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg in valueFlags) {
|
|
52
|
+
const value = args[++i];
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
throw new Error(`${arg} requires a value`);
|
|
55
|
+
values[valueFlags[arg]] = value;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
throw unknownFlagError(arg);
|
|
59
|
+
}
|
|
60
|
+
return { positional, values, flags };
|
|
61
|
+
}
|
|
62
|
+
/** Build the typed client's `ConnectOptions` from a parsed common flag set. */
|
|
63
|
+
export function connectOptionsFrom(values) {
|
|
64
|
+
const options = {};
|
|
65
|
+
if (values.env !== undefined)
|
|
66
|
+
options.environment = values.env;
|
|
67
|
+
if (values.context !== undefined)
|
|
68
|
+
options.context = values.context;
|
|
69
|
+
if (values.kubeconfig !== undefined)
|
|
70
|
+
options.client = { kubeconfigPath: values.kubeconfig };
|
|
71
|
+
return options;
|
|
72
|
+
}
|
|
73
|
+
/** kubectl duration shorthand: "3600" (bare seconds), "1h", "90m", "45s". Shared by `logs --since` and `wait --timeout`. */
|
|
74
|
+
export function parseDurationSeconds(value) {
|
|
75
|
+
const match = /^(\d+)(s|m|h)?$/.exec(value.trim());
|
|
76
|
+
if (!match)
|
|
77
|
+
throw new Error(`expected a duration like "1h", "90m", "45s", or a bare second count, got "${value}"`);
|
|
78
|
+
const n = Number(match[1]);
|
|
79
|
+
switch (match[2]) {
|
|
80
|
+
case "h":
|
|
81
|
+
return n * 3600;
|
|
82
|
+
case "m":
|
|
83
|
+
return n * 60;
|
|
84
|
+
default:
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** {@link parseDurationSeconds} in milliseconds. */
|
|
89
|
+
export function parseDurationMs(value) {
|
|
90
|
+
return parseDurationSeconds(value) * 1000;
|
|
91
|
+
}
|