@intentius/chant-lexicon-k8s 0.52.2 → 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/kustomize/rendered-entity.d.ts +11 -23
- package/dist/kustomize/rendered-entity.d.ts.map +1 -1
- package/dist/manifest-entity.d.ts +52 -0
- package/dist/manifest-entity.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/whoami.d.ts +43 -0
- package/dist/whoami.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/kustomize/rendered-entity.ts +12 -44
- package/src/manifest-entity.test.ts +78 -0
- package/src/manifest-entity.ts +82 -0
- package/src/plugin.ts +11 -0
- package/src/serializer.ts +1 -1
- package/src/whoami.test.ts +203 -0
- package/src/whoami.ts +122 -0
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
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.
|
|
6
|
-
* carries one into the build is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
24
|
-
|
|
25
|
-
export
|
|
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
|
|
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"}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/whoami.d.ts
ADDED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
6
|
-
* carries one into the build is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
24
|
-
import {
|
|
16
|
+
import { manifestEntity, RENDERED_MANIFEST_MARKER, isRenderedManifestEntity } from "../manifest-entity";
|
|
17
|
+
import type { RenderedManifestEntity } from "../manifest-entity";
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
export
|
|
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
|
}
|
|
@@ -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 "./
|
|
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
|
+
}
|