@intentius/chant-lexicon-k8s 0.52.1 → 0.53.0

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/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-lab
8
8
  export { K8sLabels, K8sAnnotations } from "./variables.js";
9
9
  export { GENERATED_ONCE_LABEL_KEY, GENERATED_ONCE_LABEL_VALUE, isGeneratedOnce } from "./secret-labels.js";
10
10
  export { microTime, isMicroTimeFormatted } from "./micro-time.js";
11
+ export { k8sManifest } from "./manifest-entity.js";
12
+ export type { RenderedManifestEntity } from "./manifest-entity.js";
11
13
  export * from "./generated/index.js";
12
14
  export { WebApp, StatefulApp, CronWorkload, AutoscaledService, WorkerPool, NamespaceEnv, NodeAgent, BatchJob, SecureIngress, ConfiguredApp, SidecarApp, MonitoredService, NetworkIsolatedApp, IrsaServiceAccount, AlbIngress, EbsStorageClass, EfsStorageClass, FluentBitAgent, ExternalDnsAgent, AdotCollector, MetricsServer, WorkloadIdentityServiceAccount, GcePdStorageClass, FilestoreStorageClass, GkeGateway, ConfigConnectorContext, GceIngress, CockroachDbCluster, CockroachDbRegionStack, RayCluster, RayJob, RayService, ArgoAppFor, ArgoAppSetForRegions, registerArgoCluster, FluxGitSource, FluxAppFor, AgicIngress, AzureDiskStorageClass, AzureFileStorageClass, AzureMonitorCollector, AksWorkloadIdentityServiceAccount, GkeFluentBitAgent, GkeOtelCollector, GkeExternalDnsAgent, AksExternalDnsAgent, Model, resolveModelStorageUri, OperatorStack, deriveHostVerbClass, DEFAULT_RESOURCE_RULES, } from "./composites/index.js";
13
15
  export type { WebAppProps, WebAppResult, StatefulAppProps, StatefulAppResult, CronWorkloadProps, CronWorkloadResult, AutoscaledServiceProps, AutoscaledServiceResult, WorkerPoolProps, WorkerPoolResult, NamespaceEnvProps, NamespaceEnvResult, NodeAgentProps, NodeAgentResult, BatchJobProps, BatchJobResult, SecureIngressProps, SecureIngressResult, ConfiguredAppProps, ConfiguredAppResult, SidecarAppProps, SidecarAppResult, MonitoredServiceProps, MonitoredServiceResult, NetworkIsolatedAppProps, NetworkIsolatedAppResult, IrsaServiceAccountProps, IrsaServiceAccountResult, AlbIngressProps, AlbIngressResult, EbsStorageClassProps, EbsStorageClassResult, EfsStorageClassProps, EfsStorageClassResult, FluentBitAgentProps, FluentBitAgentResult, ExternalDnsAgentProps, ExternalDnsAgentResult, AdotCollectorProps, AdotCollectorResult, MetricsServerProps, MetricsServerResult, WorkloadIdentityServiceAccountProps, WorkloadIdentityServiceAccountResult, GcePdStorageClassProps, GcePdStorageClassResult, FilestoreStorageClassProps, FilestoreStorageClassResult, GkeGatewayProps, GkeGatewayResult, ConfigConnectorContextProps, ConfigConnectorContextResult, GceIngressProps, GceIngressResult, CockroachDbClusterProps, CockroachDbClusterResult, CockroachDbRegionStackConfig, CockroachDbRegionCockroachConfig, CockroachDbRegionTlsConfig, RayClusterProps, RayClusterResult, RayClusterSpec, ResourceSpec, HeadGroupSpec, WorkerGroupSpec, RayJobProps, RayJobResult, RayServiceProps, RayServiceResult, ArgoDestination, ArgoSyncPolicy, ArgoAppForOptions, ArgoAppForResult, ArgoRegionTarget, ArgoAppSetForRegionsOptions, ArgoAppSetForRegionsResult, RegisterArgoClusterOptions, RegisterArgoClusterResult, FluxSourceKind, FluxSourceRef, FluxGitSourceOptions, FluxGitSourceResult, FluxAppForOptions, FluxAppForResult, AgicIngressProps, AgicIngressResult, AzureDiskStorageClassProps, AzureDiskStorageClassResult, AzureFileStorageClassProps, AzureFileStorageClassResult, AzureMonitorCollectorProps, AzureMonitorCollectorResult, AksWorkloadIdentityServiceAccountProps, AksWorkloadIdentityServiceAccountResult, GkeFluentBitAgentProps, GkeFluentBitAgentResult, GkeOtelCollectorProps, GkeOtelCollectorResult, GkeExternalDnsAgentProps, GkeExternalDnsAgentResult, AksExternalDnsAgentProps, AksExternalDnsAgentResult, ModelSource, ModelProps, ModelResult, OperatorDial, OperatorRbacResourceRule, OperatorStackConvergeHost, OperatorStackConfig, OperatorStackResult, } from "./composites/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "b67cd487ee48d5e6dc6b802e5ba186957c69312202215c3edac385532dcd6dfb",
4
+ "manifest.json": "2bda115effa437642acdca7ec48284f807cced7938f3e0312c47ace0480ffe55",
5
5
  "meta.json": "8b999bc142f448b0b627ecdc9e2388bbe672c8a1f7aae5303135624cf2fe65e3",
6
6
  "types/index.d.ts": "5f67de738adb455805e72600948940af9c664283785fb68c9fd8f5ea90d42cc3",
7
7
  "rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
@@ -65,5 +65,5 @@
65
65
  "skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5",
66
66
  "skills/chant-k8s-flux.md": "9670efc2e9854bd14686b5adeb707396b9030a4dcaee6529b30abf56efe2c50c"
67
67
  },
68
- "composite": "99445b1bd18103cd0d1584e80bf49578672912d4e294ee9391542d3eb4a6d68e"
68
+ "composite": "eca721f5280332bb6ec7cc3060dadf6619918162e92facd25c25477f7fd5b4ea"
69
69
  }
@@ -2,35 +2,23 @@
2
2
  * A rendered kustomize document as a chant entity (#1548 piece 3).
3
3
  *
4
4
  * A kustomize build root's output is a set of finished manifests, not typed
5
- * chant source — the overlay already decided every field. So the entity that
6
- * carries one into the build is deliberately verbatim: `props` IS the
7
- * document, and the serializer emits it as-is (plus the same default-label /
8
- * ownership merge every discovered resource gets) instead of running the
9
- * spec-inference heuristics built for typed declarables. The marker is what
10
- * tells the serializer to take that path — `Symbol.for`, so a lexicon built
11
- * against a separate copy of core still matches (the #1137 lesson).
12
- *
13
- * `entityType` uses the same `K8s::{Group}::{Kind}` convention the generated
14
- * operation surface speaks, so `lifecycle diff --live` observes a rendered
15
- * Deployment through exactly the reader a declared one uses. A kind the
16
- * surface doesn't know reports NOT-OBSERVED with the existing "no generated
17
- * operation surface" reason — the honest verdict, not a silent hole.
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.
18
10
  *
19
11
  * Provenance: every rendered document is stamped with an annotation naming
20
12
  * the overlay dir it came from, so a consumer (behold#171's overlay boxes)
21
- * can group by declared origin instead of guessing from paths.
13
+ * can group by declared origin instead of guessing from paths — the one thing
14
+ * this adds over a plain manifest entity.
22
15
  */
23
- import { type Declarable } from "@intentius/chant/declarable";
24
- /** Marks an entity whose `props` are a complete, render-final manifest. */
25
- export declare const RENDERED_MANIFEST_MARKER: unique symbol;
16
+ import { RENDERED_MANIFEST_MARKER, isRenderedManifestEntity } from "../manifest-entity.js";
17
+ import type { RenderedManifestEntity } from "../manifest-entity.js";
18
+ export { RENDERED_MANIFEST_MARKER, isRenderedManifestEntity };
19
+ export type { RenderedManifestEntity };
26
20
  /** Annotation stamped on every rendered document, valued with the root dir. */
27
21
  export declare const KUSTOMIZE_ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
28
- export interface RenderedManifestEntity extends Declarable {
29
- readonly kind: "resource";
30
- readonly props: Record<string, unknown>;
31
- readonly [RENDERED_MANIFEST_MARKER]: true;
32
- }
33
- export declare function isRenderedManifestEntity(entity: Declarable): entity is RenderedManifestEntity;
34
22
  /**
35
23
  * Wrap one rendered document. Returns null when the document has no string
36
24
  * `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
@@ -1 +1 @@
1
- {"version":3,"file":"rendered-entity.d.ts","sourceRoot":"","sources":["../../src/kustomize/rendered-entity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAGjF,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,eAA2C,CAAC;AAEjF,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAE7E,MAAM,WAAW,sBAAuB,SAAQ,UAAU;IACxD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,CAAC,wBAAwB,CAAC,EAAE,IAAI,CAAC;CAC3C;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI,sBAAsB,CAE7F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,GACX,sBAAsB,GAAG,IAAI,CAuB/B"}
1
+ {"version":3,"file":"rendered-entity.d.ts","sourceRoot":"","sources":["../../src/kustomize/rendered-entity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAkB,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACxG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,CAAC;AAC9D,YAAY,EAAE,sBAAsB,EAAE,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAE7E;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,GACX,sBAAsB,GAAG,IAAI,CAQ/B"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A complete Kubernetes manifest carried into a build as a chant entity.
3
+ *
4
+ * Two callers produce one: a kustomize build root, whose output is a set of
5
+ * finished documents rather than typed chant source (#1548), and `chant carve
6
+ * emit` adopting a Terraform `kubernetes_manifest` (#999), whose whole content
7
+ * is a manifest body read out of state. In both cases the document already
8
+ * decided every field, so `props` IS the manifest: the serializer emits it
9
+ * as-is — plus the default-label / ownership merge every discovered resource
10
+ * gets — instead of running the spec-inference heuristics built for typed
11
+ * declarables, which would re-nest a top-level field that is not `spec`
12
+ * (`rules` on a ClusterRole, `webhooks` on a webhook config).
13
+ *
14
+ * The marker is what tells the serializer to take that path — `Symbol.for`, so
15
+ * a lexicon built against a separate copy of core still matches (the #1137
16
+ * lesson).
17
+ *
18
+ * `entityType` comes from `gvkToTypeName`, the one group→namespace rule
19
+ * (`./group-namespace`), so a manifest declared this way is typed exactly as a
20
+ * generated class would be and `lifecycle diff --live` observes it through the
21
+ * reader a declared resource uses. A kind the generated operation surface does
22
+ * not know reports NOT-OBSERVED with the existing "no generated operation
23
+ * surface" reason — the honest verdict, not a silent hole.
24
+ */
25
+ import { type Declarable } from "@intentius/chant/declarable";
26
+ /** Marks an entity whose `props` are a complete, render-final manifest. */
27
+ export declare const RENDERED_MANIFEST_MARKER: unique symbol;
28
+ export interface RenderedManifestEntity extends Declarable {
29
+ readonly kind: "resource";
30
+ readonly props: Record<string, unknown>;
31
+ readonly [RENDERED_MANIFEST_MARKER]: true;
32
+ }
33
+ export declare function isRenderedManifestEntity(entity: Declarable): entity is RenderedManifestEntity;
34
+ /**
35
+ * Wrap one manifest. Returns null when the document has no string
36
+ * `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
37
+ */
38
+ export declare function manifestEntity(doc: Record<string, unknown>): RenderedManifestEntity | null;
39
+ /**
40
+ * Declare an arbitrary Kubernetes object from its manifest — the escape hatch
41
+ * for a kind the lexicon ships no generated class for, and what `chant carve
42
+ * emit` writes when it adopts a `kubernetes_manifest` (#999). The typed
43
+ * constructors cannot serve either case: their props are the kind's own schema,
44
+ * so an unknown CRD has no class at all and a known one rejects an
45
+ * `apiVersion`/`kind` pair that differs from the generated default.
46
+ *
47
+ * Throws on a document without a string `apiVersion` and `kind`: those two
48
+ * fields are what makes it addressable, and a silently dropped resource is the
49
+ * failure this is guarding against.
50
+ */
51
+ export declare function k8sManifest(doc: Record<string, unknown>): RenderedManifestEntity;
52
+ //# sourceMappingURL=manifest-entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-entity.d.ts","sourceRoot":"","sources":["../src/manifest-entity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAGjF,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,eAA2C,CAAC;AAEjF,MAAM,WAAW,sBAAuB,SAAQ,UAAU;IACxD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,CAAC,wBAAwB,CAAC,EAAE,IAAI,CAAC;CAC3C;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI,sBAAsB,CAE7F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,GAAG,IAAI,CAiB1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,CAMhF"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.52.1",
3
+ "version": "0.53.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AA6BjG,eAAO,MAAM,SAAS,EAAE,aAmsBvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AA6BjG,eAAO,MAAM,SAAS,EAAE,aA8sBvB,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Who chant would act as in a cluster — the lexicon half of `chant lifecycle
3
+ * whoami` (chant #1982).
4
+ *
5
+ * The connector already resolves all of this and hands it back on every
6
+ * observation: `defaultK8sConnector` picks a context from
7
+ * `k8s.profiles.<env>.context` or from the ambient kubeconfig, the client
8
+ * records the server it resolved and which credential path authorized it, and
9
+ * `describeResources` then throws the lot away. This reports it, and reports
10
+ * it BEFORE a read rather than after a wrong one.
11
+ *
12
+ * Same connector, same environment, same options, so the server named here is
13
+ * the server the read talks to. A test pins that: a whoami that names a
14
+ * cluster the read does not use would be worse than no whoami at all.
15
+ *
16
+ * ## The principal is the API server's answer, not the kubeconfig's
17
+ *
18
+ * A kubeconfig `user` entry is a local alias. The cluster may map that
19
+ * credential onto any subject it likes, which is exactly why "am I acting as
20
+ * the identity I think I am" is a real question. So the principal comes from
21
+ * `SelfSubjectReview` — the call behind `kubectl auth whoami` — which the API
22
+ * server answers with the subject it authenticated. It creates nothing.
23
+ *
24
+ * A cluster older than the review API serves no version of it. That is
25
+ * `read-failed` with a detail saying so, not a fallback to the local alias
26
+ * dressed up as an identity.
27
+ *
28
+ * ## No credential in the answer
29
+ *
30
+ * The credential path is reported by NAME — `exec-plugin` and its command,
31
+ * `token`, `client-certificate`. A kubeconfig holding a static bearer token is
32
+ * the case where the identity signal IS a secret, and the report says
33
+ * `credential: token` and stops there. The token, the client key and the exec
34
+ * plugin's output never leave the client.
35
+ */
36
+ import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
37
+ import { type K8sConnector } from "./api/connect.js";
38
+ /**
39
+ * The k8s `describeIdentity`. Connects the way the read connects, asks the API
40
+ * server who it is talking to, and reports the context that binding came from.
41
+ */
42
+ export declare function describeIdentity(options: DescribeIdentityOptions, connect?: K8sConnector): Promise<DescribeIdentityResult>;
43
+ //# sourceMappingURL=whoami.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../src/whoami.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAQvE;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,EAChC,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,sBAAsB,CAAC,CAqEjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.52.1",
3
+ "version": "0.53.0",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -76,7 +76,7 @@
76
76
  "@types/js-yaml": "^4.0.9"
77
77
  },
78
78
  "optionalDependencies": {
79
- "@intentius/chant-k8s-client": "^0.52.1"
79
+ "@intentius/chant-k8s-client": "^0.53.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@intentius/chant": "*",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  "peerDependencies": {
86
86
  "zod": "^4.3.6",
87
- "@intentius/chant": "^0.52.1",
87
+ "@intentius/chant": "^0.53.0",
88
88
  "typescript": "^5.9.3"
89
89
  }
90
90
  }
package/src/index.ts CHANGED
@@ -34,6 +34,11 @@ export { GENERATED_ONCE_LABEL_KEY, GENERATED_ONCE_LABEL_VALUE, isGeneratedOnce }
34
34
  // MicroTime helpers — see micro-time.ts for why this exists.
35
35
  export { microTime, isMicroTimeFormatted } from "./micro-time";
36
36
 
37
+ // Verbatim manifest escape hatch — a kind with no generated class, and what
38
+ // `chant carve emit` writes for a Terraform kubernetes_manifest (#999).
39
+ export { k8sManifest } from "./manifest-entity";
40
+ export type { RenderedManifestEntity } from "./manifest-entity";
41
+
37
42
  // Generated entities — export everything from generated index
38
43
  // After running `chant generate`, this re-exports all K8s resource classes
39
44
  export * from "./generated/index";
@@ -2,43 +2,26 @@
2
2
  * A rendered kustomize document as a chant entity (#1548 piece 3).
3
3
  *
4
4
  * A kustomize build root's output is a set of finished manifests, not typed
5
- * chant source — the overlay already decided every field. So the entity that
6
- * carries one into the build is deliberately verbatim: `props` IS the
7
- * document, and the serializer emits it as-is (plus the same default-label /
8
- * ownership merge every discovered resource gets) instead of running the
9
- * spec-inference heuristics built for typed declarables. The marker is what
10
- * tells the serializer to take that path — `Symbol.for`, so a lexicon built
11
- * against a separate copy of core still matches (the #1137 lesson).
12
- *
13
- * `entityType` uses the same `K8s::{Group}::{Kind}` convention the generated
14
- * operation surface speaks, so `lifecycle diff --live` observes a rendered
15
- * Deployment through exactly the reader a declared one uses. A kind the
16
- * surface doesn't know reports NOT-OBSERVED with the existing "no generated
17
- * operation surface" reason — the honest verdict, not a silent hole.
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.
18
10
  *
19
11
  * Provenance: every rendered document is stamped with an annotation naming
20
12
  * the overlay dir it came from, so a consumer (behold#171's overlay boxes)
21
- * can group by declared origin instead of guessing from paths.
13
+ * can group by declared origin instead of guessing from paths — the one thing
14
+ * this adds over a plain manifest entity.
22
15
  */
23
- import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
24
- import { gvkToTypeName } from "../spec/parse";
16
+ import { manifestEntity, RENDERED_MANIFEST_MARKER, isRenderedManifestEntity } from "../manifest-entity";
17
+ import type { RenderedManifestEntity } from "../manifest-entity";
25
18
 
26
- /** Marks an entity whose `props` are a complete, render-final manifest. */
27
- export const RENDERED_MANIFEST_MARKER = Symbol.for("chant.k8s.renderedManifest");
19
+ export { RENDERED_MANIFEST_MARKER, isRenderedManifestEntity };
20
+ export type { RenderedManifestEntity };
28
21
 
29
22
  /** Annotation stamped on every rendered document, valued with the root dir. */
30
23
  export const KUSTOMIZE_ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
31
24
 
32
- export interface RenderedManifestEntity extends Declarable {
33
- readonly kind: "resource";
34
- readonly props: Record<string, unknown>;
35
- readonly [RENDERED_MANIFEST_MARKER]: true;
36
- }
37
-
38
- export function isRenderedManifestEntity(entity: Declarable): entity is RenderedManifestEntity {
39
- return (entity as unknown as Record<symbol, unknown>)[RENDERED_MANIFEST_MARKER] === true;
40
- }
41
-
42
25
  /**
43
26
  * Wrap one rendered document. Returns null when the document has no string
44
27
  * `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
@@ -47,26 +30,11 @@ export function renderedManifestEntity(
47
30
  doc: Record<string, unknown>,
48
31
  root: string,
49
32
  ): RenderedManifestEntity | null {
50
- const apiVersion = doc.apiVersion;
51
- const kind = doc.kind;
52
- if (typeof apiVersion !== "string" || typeof kind !== "string" || !apiVersion || !kind) return null;
53
-
54
- const slash = apiVersion.indexOf("/");
55
- const group = slash === -1 ? "" : apiVersion.slice(0, slash);
56
- const version = slash === -1 ? apiVersion : apiVersion.slice(slash + 1);
57
-
58
33
  const metadata = { ...((doc.metadata as Record<string, unknown> | undefined) ?? {}) };
59
34
  metadata.annotations = {
60
35
  ...((metadata.annotations as Record<string, unknown> | undefined) ?? {}),
61
36
  [KUSTOMIZE_ROOT_ANNOTATION]: root,
62
37
  };
63
38
 
64
- return {
65
- lexicon: "k8s",
66
- entityType: gvkToTypeName({ group, version, kind }),
67
- kind: "resource",
68
- props: { ...doc, metadata },
69
- [DECLARABLE_MARKER]: true,
70
- [RENDERED_MANIFEST_MARKER]: true,
71
- };
39
+ return manifestEntity({ ...doc, metadata });
72
40
  }
@@ -2221,6 +2221,29 @@ describe("WK8501/WK8502: custom resource spec validated against the shipped CRD
2221
2221
  expect(wk8502.check(ctx)).toEqual([]);
2222
2222
  });
2223
2223
 
2224
+ // chant #2013 — the Argo AppProject from behold's example-argo-estate, as
2225
+ // YAML text rather than JSON on purpose: `sourceRepos` is a list of bare
2226
+ // URLs, and the core parser used to hand this check `{ https: "//github…" }`
2227
+ // for each one. WK8502 then reported an object on a field the source had
2228
+ // declared, and the emitter had written, as a string.
2229
+ test.skipIf(!hasLexicon)("a bare URL in an AppProject's sourceRepos reaches the check as a string", () => {
2230
+ const ctx = makeCtx([
2231
+ "apiVersion: argoproj.io/v1alpha1",
2232
+ "kind: AppProject",
2233
+ "metadata:",
2234
+ " name: estate",
2235
+ "spec:",
2236
+ " sourceRepos:",
2237
+ " - https://github.com/INTENTIUS/behold",
2238
+ " destinations:",
2239
+ " - namespace: '*'",
2240
+ " server: https://kubernetes.default.svc",
2241
+ "",
2242
+ ].join("\n"));
2243
+ expect(wk8501.check(ctx)).toEqual([]);
2244
+ expect(wk8502.check(ctx)).toEqual([]);
2245
+ });
2246
+
2224
2247
  test("open objects and int-or-string accept anything (stub registry)", () => {
2225
2248
  setCrdSchemaRegistry(new Map([[
2226
2249
  "example.com/v1/Widget",
@@ -0,0 +1,78 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { k8sManifest, manifestEntity, isRenderedManifestEntity } from "./manifest-entity";
3
+ import { k8sSerializer } from "./serializer";
4
+ import type { Declarable } from "@intentius/chant/declarable";
5
+
6
+ /**
7
+ * The verbatim manifest escape hatch, and the round trip `chant carve emit`
8
+ * depends on (chant #999): a `kubernetes_manifest` body adopted out of
9
+ * Terraform state has to serialize back to the same object, kind included, or
10
+ * the carve loses configuration on the way out of Terraform.
11
+ */
12
+ describe("k8sManifest", () => {
13
+ test("types itself from apiVersion + kind through the shared group rule", () => {
14
+ expect(k8sManifest({ apiVersion: "v1", kind: "ConfigMap", metadata: { name: "app-config" } }).entityType).toBe(
15
+ "K8s::Core::ConfigMap",
16
+ );
17
+ expect(k8sManifest({ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "web" } }).entityType).toBe(
18
+ "K8s::Apps::Deployment",
19
+ );
20
+ expect(k8sManifest({ apiVersion: "cert-manager.io/v1", kind: "Certificate", metadata: { name: "tls" } }).entityType).toBe(
21
+ "K8s::CertManager::Certificate",
22
+ );
23
+ // The overrides apply too — this is the one group→namespace rule, not a copy.
24
+ expect(k8sManifest({ apiVersion: "argoproj.io/v1alpha1", kind: "Application", metadata: { name: "app" } }).entityType).toBe(
25
+ "K8s::Argo::Application",
26
+ );
27
+ });
28
+
29
+ test("refuses a document that is not a Kubernetes object", () => {
30
+ expect(() => k8sManifest({ metadata: { name: "x" } })).toThrow(/apiVersion/);
31
+ expect(manifestEntity({ metadata: { name: "x" } })).toBeNull();
32
+ });
33
+
34
+ test("serializes verbatim, including a kind the lexicon ships no class for", () => {
35
+ const entities = new Map<string, Declarable>();
36
+ entities.set(
37
+ "webCert",
38
+ k8sManifest({
39
+ apiVersion: "cert-manager.io/v1",
40
+ kind: "Certificate",
41
+ metadata: { name: "web-tls", namespace: "web" },
42
+ spec: { secretName: "web-tls", dnsNames: ["web.example.com"] },
43
+ }),
44
+ );
45
+
46
+ const yaml = k8sSerializer.serialize(entities);
47
+ expect(yaml).toContain("apiVersion: cert-manager.io/v1");
48
+ expect(yaml).toContain("kind: Certificate");
49
+ expect(yaml).toContain("name: web-tls");
50
+ expect(yaml).toContain("- web.example.com");
51
+ });
52
+
53
+ test("a top-level field that is not spec stays at the top level", () => {
54
+ // The spec-inference heuristics are for typed declarables; a finished
55
+ // manifest must not have its `rules` re-nested under a synthetic spec.
56
+ const entities = new Map<string, Declarable>();
57
+ entities.set(
58
+ "viewer",
59
+ k8sManifest({
60
+ apiVersion: "rbac.authorization.k8s.io/v1",
61
+ kind: "ClusterRole",
62
+ metadata: { name: "viewer" },
63
+ rules: [{ apiGroups: [""], resources: ["pods"], verbs: ["get", "list"] }],
64
+ }),
65
+ );
66
+
67
+ const yaml = k8sSerializer.serialize(entities);
68
+ expect(yaml).toMatch(/^rules:/m);
69
+ expect(yaml).not.toMatch(/^spec:\s*\n\s+rules:/m);
70
+ });
71
+
72
+ test("is the same verbatim entity a rendered kustomize document is", () => {
73
+ const entity = k8sManifest({ apiVersion: "v1", kind: "Namespace", metadata: { name: "web" } });
74
+ expect(isRenderedManifestEntity(entity)).toBe(true);
75
+ expect(entity.lexicon).toBe("k8s");
76
+ expect(entity.kind).toBe("resource");
77
+ });
78
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A complete Kubernetes manifest carried into a build as a chant entity.
3
+ *
4
+ * Two callers produce one: a kustomize build root, whose output is a set of
5
+ * finished documents rather than typed chant source (#1548), and `chant carve
6
+ * emit` adopting a Terraform `kubernetes_manifest` (#999), whose whole content
7
+ * is a manifest body read out of state. In both cases the document already
8
+ * decided every field, so `props` IS the manifest: the serializer emits it
9
+ * as-is — plus the default-label / ownership merge every discovered resource
10
+ * gets — instead of running the spec-inference heuristics built for typed
11
+ * declarables, which would re-nest a top-level field that is not `spec`
12
+ * (`rules` on a ClusterRole, `webhooks` on a webhook config).
13
+ *
14
+ * The marker is what tells the serializer to take that path — `Symbol.for`, so
15
+ * a lexicon built against a separate copy of core still matches (the #1137
16
+ * lesson).
17
+ *
18
+ * `entityType` comes from `gvkToTypeName`, the one group→namespace rule
19
+ * (`./group-namespace`), so a manifest declared this way is typed exactly as a
20
+ * generated class would be and `lifecycle diff --live` observes it through the
21
+ * reader a declared resource uses. A kind the generated operation surface does
22
+ * not know reports NOT-OBSERVED with the existing "no generated operation
23
+ * surface" reason — the honest verdict, not a silent hole.
24
+ */
25
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
26
+ import { gvkToTypeName } from "./spec/parse";
27
+
28
+ /** Marks an entity whose `props` are a complete, render-final manifest. */
29
+ export const RENDERED_MANIFEST_MARKER = Symbol.for("chant.k8s.renderedManifest");
30
+
31
+ export interface RenderedManifestEntity extends Declarable {
32
+ readonly kind: "resource";
33
+ readonly props: Record<string, unknown>;
34
+ readonly [RENDERED_MANIFEST_MARKER]: true;
35
+ }
36
+
37
+ export function isRenderedManifestEntity(entity: Declarable): entity is RenderedManifestEntity {
38
+ return (entity as unknown as Record<symbol, unknown>)[RENDERED_MANIFEST_MARKER] === true;
39
+ }
40
+
41
+ /**
42
+ * Wrap one manifest. Returns null when the document has no string
43
+ * `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
44
+ */
45
+ export function manifestEntity(doc: Record<string, unknown>): RenderedManifestEntity | null {
46
+ const apiVersion = doc.apiVersion;
47
+ const kind = doc.kind;
48
+ if (typeof apiVersion !== "string" || typeof kind !== "string" || !apiVersion || !kind) return null;
49
+
50
+ const slash = apiVersion.indexOf("/");
51
+ const group = slash === -1 ? "" : apiVersion.slice(0, slash);
52
+ const version = slash === -1 ? apiVersion : apiVersion.slice(slash + 1);
53
+
54
+ return {
55
+ lexicon: "k8s",
56
+ entityType: gvkToTypeName({ group, version, kind }),
57
+ kind: "resource",
58
+ props: { ...doc },
59
+ [DECLARABLE_MARKER]: true,
60
+ [RENDERED_MANIFEST_MARKER]: true,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Declare an arbitrary Kubernetes object from its manifest — the escape hatch
66
+ * for a kind the lexicon ships no generated class for, and what `chant carve
67
+ * emit` writes when it adopts a `kubernetes_manifest` (#999). The typed
68
+ * constructors cannot serve either case: their props are the kind's own schema,
69
+ * so an unknown CRD has no class at all and a known one rejects an
70
+ * `apiVersion`/`kind` pair that differs from the generated default.
71
+ *
72
+ * Throws on a document without a string `apiVersion` and `kind`: those two
73
+ * fields are what makes it addressable, and a silently dropped resource is the
74
+ * failure this is guarding against.
75
+ */
76
+ export function k8sManifest(doc: Record<string, unknown>): RenderedManifestEntity {
77
+ const entity = manifestEntity(doc);
78
+ if (!entity) {
79
+ throw new Error("k8sManifest requires a document with string `apiVersion` and `kind` fields.");
80
+ }
81
+ return entity;
82
+ }
package/src/plugin.ts CHANGED
@@ -697,6 +697,17 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
697
697
  return describeResources(options);
698
698
  },
699
699
 
700
+ /**
701
+ * Who chant would act as in this cluster, before it acts (#1982) — the
702
+ * connector's own context/credential resolution plus the API server's
703
+ * `SelfSubjectReview`, reported through the same connect path the read uses
704
+ * so the cluster named is the cluster read. Implementation in ./whoami.ts.
705
+ */
706
+ async describeIdentity(options) {
707
+ const { describeIdentity } = await import("./whoami");
708
+ return describeIdentity(options);
709
+ },
710
+
700
711
  // Env teardown (#1222): enumeration + execution over the marker label
701
712
  // selector (managed-by + stack + env), through the same typed client the
702
713
  // prune uses. Execution deletes namespaces last. See ./teardown.ts.
package/src/serializer.ts CHANGED
@@ -14,7 +14,7 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
14
14
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
15
15
  import { emitYAML } from "@intentius/chant/yaml";
16
16
  import { isDefaultLabels, isDefaultAnnotations, type DefaultLabels, type DefaultAnnotations } from "./default-labels";
17
- import { isRenderedManifestEntity } from "./kustomize/rendered-entity";
17
+ import { isRenderedManifestEntity } from "./manifest-entity";
18
18
  import { isEncryptedSecretFileEntity } from "./sops/entity";
19
19
 
20
20
  const require = createRequire(import.meta.url);
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `chant lifecycle whoami` for Kubernetes (chant #1982).
3
+ *
4
+ * Every case drives the real client against a literal kubeconfig with the
5
+ * transport faked, so no ambient kubeconfig is read and no cluster is
6
+ * contacted. The load-bearing case is the last one: whoami and
7
+ * `describeResources` must resolve the same cluster, or the report describes a
8
+ * binding the read does not use.
9
+ */
10
+
11
+ import { describe, test, expect, vi, beforeEach } from "vitest";
12
+
13
+ const loadChantConfigMock = vi.fn();
14
+ vi.mock("@intentius/chant/config", () => ({
15
+ loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
16
+ }));
17
+
18
+ const { describeIdentity } = await import("./whoami");
19
+ const { describeResources } = await import("./describe-resources");
20
+ const { fakeCluster, objectKey } = await import("./api/fake-cluster");
21
+ const { defaultK8sConnector } = await import("./api/connect");
22
+ const { fakeKubeconfig, statusBody } = await import("@intentius/chant-k8s-client/testing");
23
+ import type { RecordedRequest } from "@intentius/chant-k8s-client/testing";
24
+ import type { ResolvedIdentity, UnresolvedIdentity } from "@intentius/chant/lexicon";
25
+
26
+ const REVIEW_V1 = "/apis/authentication.k8s.io/v1/selfsubjectreviews";
27
+
28
+ /** The API server's own answer to "who am I". */
29
+ function reviewResponse(username: string): { body: Record<string, unknown> } {
30
+ return {
31
+ body: {
32
+ apiVersion: "authentication.k8s.io/v1",
33
+ kind: "SelfSubjectReview",
34
+ status: { userInfo: { username, groups: ["system:authenticated"] } },
35
+ },
36
+ };
37
+ }
38
+
39
+ const twoContexts = fakeKubeconfig({
40
+ contexts: [
41
+ { name: "prod-eks", cluster: "prod", user: "prod-user", namespace: "chant" },
42
+ { name: "staging-eks", cluster: "staging", user: "staging-user" },
43
+ ],
44
+ currentContext: "staging-eks",
45
+ server: "https://cluster.test:6443",
46
+ });
47
+
48
+ const resolved = (r: unknown): ResolvedIdentity => r as ResolvedIdentity;
49
+ const refusal = (r: unknown): UnresolvedIdentity => (r as { unresolved: UnresolvedIdentity }).unresolved;
50
+
51
+ describe("k8s describeIdentity (#1982)", () => {
52
+ beforeEach(() => {
53
+ loadChantConfigMock.mockReset();
54
+ loadChantConfigMock.mockResolvedValue({ config: {} });
55
+ });
56
+
57
+ test("reports the API server's subject, the bound context as scope, and the binding as source", async () => {
58
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
59
+ const cluster = fakeCluster({
60
+ kubeconfig: twoContexts,
61
+ respond: (req: RecordedRequest) => (req.path === REVIEW_V1 ? reviewResponse("system:serviceaccount:chant:deployer") : undefined),
62
+ });
63
+
64
+ const result = resolved(
65
+ await describeIdentity({ environment: "prod" }, (o) =>
66
+ defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
67
+ ),
68
+ );
69
+
70
+ expect(result.identity).toBe("system:serviceaccount:chant:deployer");
71
+ expect(result.scope).toBe("prod-eks ns=chant");
72
+ expect(result.source).toContain("k8s.profiles.prod.context");
73
+ expect(result.source).toContain("credential token");
74
+ expect(result.endpoint).toBe("https://cluster.test:6443");
75
+ });
76
+
77
+ test("an unbound environment says which binding is missing rather than staying quiet", async () => {
78
+ const cluster = fakeCluster({
79
+ kubeconfig: twoContexts,
80
+ respond: (req: RecordedRequest) => (req.path === REVIEW_V1 ? reviewResponse("kubernetes-admin") : undefined),
81
+ });
82
+
83
+ const result = resolved(
84
+ await describeIdentity({ environment: "prod" }, (o) =>
85
+ defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
86
+ ),
87
+ );
88
+
89
+ expect(result.identity).toBe("kubernetes-admin");
90
+ expect(result.scope).toContain("staging-eks");
91
+ expect(result.source).toContain("no k8s.profiles.prod binding");
92
+ });
93
+
94
+ test("a cluster with no review API is 'could not determine', not the kubeconfig's local alias", async () => {
95
+ // The kubeconfig user is `prod-user` here. Reporting that as the identity
96
+ // would be a guess: the cluster maps a credential onto whatever subject it
97
+ // likes, which is the whole reason the question is worth asking.
98
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
99
+ const cluster = fakeCluster({ kubeconfig: twoContexts });
100
+
101
+ const result = refusal(
102
+ await describeIdentity({ environment: "prod" }, (o) =>
103
+ defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
104
+ ),
105
+ );
106
+
107
+ expect(result.reason).toBe("read-failed");
108
+ expect(result.detail).toContain("SelfSubjectReview");
109
+ expect(result.detail).toContain("local credential entry");
110
+ });
111
+
112
+ test("a refused review is no-credentials, keeping it apart from a broken one", async () => {
113
+ const cluster = fakeCluster({
114
+ kubeconfig: twoContexts,
115
+ respond: (req: RecordedRequest) =>
116
+ req.path === REVIEW_V1
117
+ ? { status: 403, body: statusBody(403, "Forbidden", "selfsubjectreviews is forbidden") }
118
+ : undefined,
119
+ });
120
+
121
+ const result = refusal(
122
+ await describeIdentity({ environment: "prod" }, (o) =>
123
+ defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
124
+ ),
125
+ );
126
+ expect(result.reason).toBe("no-credentials");
127
+ });
128
+
129
+ test("a binding naming a context the kubeconfig does not have is no-binding", async () => {
130
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "missing-eks" } } } } });
131
+ const cluster = fakeCluster({ kubeconfig: twoContexts });
132
+
133
+ const result = refusal(
134
+ await describeIdentity({ environment: "prod" }, (o) =>
135
+ defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
136
+ ),
137
+ );
138
+ expect(result.reason).toBe("no-binding");
139
+ expect(result.detail).toContain("missing-eks");
140
+ });
141
+
142
+ test("the credential is named, never printed", async () => {
143
+ const withToken = fakeKubeconfig({ token: "a-static-bearer-token-value" });
144
+ const cluster = fakeCluster({
145
+ kubeconfig: withToken,
146
+ respond: (req: RecordedRequest) => (req.path === REVIEW_V1 ? reviewResponse("ci@acme.example") : undefined),
147
+ });
148
+
149
+ const result = await describeIdentity({ environment: "prod" }, (o) =>
150
+ defaultK8sConnector({ ...o, client: { kubeconfig: withToken, requestLayer: cluster.layer } }),
151
+ );
152
+ // A kubeconfig holding a static token is the case where the identity
153
+ // signal IS a secret. The report says `credential token` and stops.
154
+ expect(resolved(result).source).toContain("credential token");
155
+ expect(JSON.stringify(result)).not.toContain("a-static-bearer-token-value");
156
+ });
157
+ });
158
+
159
+ describe("whoami and the live read resolve the same cluster (#1982 acceptance)", () => {
160
+ test("the reported endpoint is the server describeResources reads against", async () => {
161
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
162
+ const web = {
163
+ apiVersion: "apps/v1",
164
+ kind: "Deployment",
165
+ metadata: { name: "web", namespace: "chant", uid: "uid-1", resourceVersion: "7" },
166
+ status: { readyReplicas: 1, replicas: 1 },
167
+ };
168
+ const cluster = fakeCluster({
169
+ kubeconfig: twoContexts,
170
+ objects: { [objectKey("apps/v1", "Deployment", "web", "chant")]: web },
171
+ respond: (req: RecordedRequest) => (req.path === REVIEW_V1 ? reviewResponse("system:serviceaccount:chant:deployer") : undefined),
172
+ });
173
+ const connects: Array<string | undefined> = [];
174
+ const connect = (o: Parameters<typeof defaultK8sConnector>[0]) => {
175
+ connects.push(o.environment);
176
+ return defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } });
177
+ };
178
+
179
+ const identity = resolved(await describeIdentity({ environment: "prod" }, connect));
180
+ const reviewUrl = cluster.layer.requests.find((r) => r.path === REVIEW_V1)!.url;
181
+
182
+ cluster.layer.requests.length = 0;
183
+ const observed = await describeResources(
184
+ {
185
+ environment: "prod",
186
+ buildOutput: "",
187
+ entityNames: ["web"],
188
+ entities: new Map([["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "chant" } } }]]),
189
+ },
190
+ connect,
191
+ );
192
+ expect(observed.resources.web).toBeDefined();
193
+
194
+ const origin = (url: string): string => new URL(url).origin;
195
+ expect(origin(reviewUrl)).toBe(identity.endpoint);
196
+ for (const request of cluster.layer.requests) {
197
+ expect(origin(request.url)).toBe(identity.endpoint);
198
+ }
199
+ // Both connect calls resolved the same environment through the same
200
+ // connector, so the binding cannot diverge between the two.
201
+ expect(connects).toEqual(["prod", "prod"]);
202
+ });
203
+ });
package/src/whoami.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Who chant would act as in a cluster — the lexicon half of `chant lifecycle
3
+ * whoami` (chant #1982).
4
+ *
5
+ * The connector already resolves all of this and hands it back on every
6
+ * observation: `defaultK8sConnector` picks a context from
7
+ * `k8s.profiles.<env>.context` or from the ambient kubeconfig, the client
8
+ * records the server it resolved and which credential path authorized it, and
9
+ * `describeResources` then throws the lot away. This reports it, and reports
10
+ * it BEFORE a read rather than after a wrong one.
11
+ *
12
+ * Same connector, same environment, same options, so the server named here is
13
+ * the server the read talks to. A test pins that: a whoami that names a
14
+ * cluster the read does not use would be worse than no whoami at all.
15
+ *
16
+ * ## The principal is the API server's answer, not the kubeconfig's
17
+ *
18
+ * A kubeconfig `user` entry is a local alias. The cluster may map that
19
+ * credential onto any subject it likes, which is exactly why "am I acting as
20
+ * the identity I think I am" is a real question. So the principal comes from
21
+ * `SelfSubjectReview` — the call behind `kubectl auth whoami` — which the API
22
+ * server answers with the subject it authenticated. It creates nothing.
23
+ *
24
+ * A cluster older than the review API serves no version of it. That is
25
+ * `read-failed` with a detail saying so, not a fallback to the local alias
26
+ * dressed up as an identity.
27
+ *
28
+ * ## No credential in the answer
29
+ *
30
+ * The credential path is reported by NAME — `exec-plugin` and its command,
31
+ * `token`, `client-certificate`. A kubeconfig holding a static bearer token is
32
+ * the case where the identity signal IS a secret, and the report says
33
+ * `credential: token` and stops there. The token, the client key and the exec
34
+ * plugin's output never leave the client.
35
+ */
36
+
37
+ import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
38
+ import { defaultK8sConnector, type K8sConnector } from "./api/connect";
39
+ import { classifyApiFailure, isMissingClientPackage, MISSING_CLIENT_DETAIL } from "./api/classify";
40
+
41
+ /** Everything about the caller's credential except the credential. */
42
+ function credentialNote(credential: string, execCommand?: string): string {
43
+ return execCommand ? `credential ${credential} (${execCommand})` : `credential ${credential}`;
44
+ }
45
+
46
+ /**
47
+ * The k8s `describeIdentity`. Connects the way the read connects, asks the API
48
+ * server who it is talking to, and reports the context that binding came from.
49
+ */
50
+ export async function describeIdentity(
51
+ options: DescribeIdentityOptions,
52
+ connect: K8sConnector = defaultK8sConnector,
53
+ ): Promise<DescribeIdentityResult> {
54
+ let connected;
55
+ try {
56
+ connected = await connect({ environment: options.environment, cwd: options.cwd });
57
+ } catch (err) {
58
+ if (isMissingClientPackage(err)) {
59
+ return { unresolved: { reason: "read-failed", detail: MISSING_CLIENT_DETAIL } };
60
+ }
61
+ const outcome = classifyApiFailure(err);
62
+ return {
63
+ unresolved:
64
+ outcome.kind === "unobserved"
65
+ ? { reason: outcome.reason, detail: outcome.detail }
66
+ : { reason: "no-binding", detail: err instanceof Error ? err.message : String(err) },
67
+ };
68
+ }
69
+
70
+ const { client, target } = connected;
71
+ const provenance = client.provenance;
72
+ const context = provenance.context ?? "(unset)";
73
+ const scope = `${context} ns=${client.defaultNamespace}`;
74
+ // Where the context came from, in the project's own vocabulary. An ambient
75
+ // context is not a failure — but it is the one an operator most often did
76
+ // not mean, so it says which binding is missing.
77
+ const binding =
78
+ target.source === "bound"
79
+ ? `k8s.profiles.${options.environment}.context`
80
+ : `ambient kubeconfig current-context (no k8s.profiles.${options.environment} binding)`;
81
+ const source = [
82
+ binding,
83
+ credentialNote(provenance.credential, provenance.execCommand),
84
+ `kubeconfig ${provenance.kubeconfigSource}`,
85
+ ].join("; ");
86
+
87
+ let subject;
88
+ try {
89
+ subject = await client.selfSubjectReview();
90
+ } catch (err) {
91
+ const outcome = classifyApiFailure(err);
92
+ return {
93
+ unresolved: {
94
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
95
+ detail:
96
+ outcome.kind === "unobserved"
97
+ ? `SelfSubjectReview against ${provenance.server}: ${outcome.detail}`
98
+ : `SelfSubjectReview against ${provenance.server} did not answer`,
99
+ },
100
+ };
101
+ }
102
+
103
+ if (!subject) {
104
+ return {
105
+ unresolved: {
106
+ reason: "read-failed",
107
+ detail:
108
+ `${provenance.server} serves no authentication.k8s.io SelfSubjectReview, so the API server ` +
109
+ `cannot be asked who it authenticated (the API landed in Kubernetes 1.26). The kubeconfig ` +
110
+ `context is "${context}" with ${credentialNote(provenance.credential, provenance.execCommand)}, ` +
111
+ `which names a local credential entry, not the subject the cluster resolves it to.`,
112
+ },
113
+ };
114
+ }
115
+
116
+ return {
117
+ identity: subject.username,
118
+ scope,
119
+ source,
120
+ endpoint: provenance.server,
121
+ };
122
+ }