@intentius/chant-k8s-client 0.42.1 → 0.44.2

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/client.d.ts CHANGED
@@ -125,6 +125,18 @@ export interface K8sClient {
125
125
  resolve(selector: ResourceSelector, signal?: AbortSignal): Promise<ApiResourceInfo | undefined>;
126
126
  /** GET one object. Throws {@link K8sApiError} with `notFound` when absent. */
127
127
  read(ref: ObjectRef, options?: ReadOptions): Promise<K8sObject>;
128
+ /**
129
+ * The exact request path {@link read} would GET for this ref — apiVersion and
130
+ * kind resolved against the cluster's own discovery (which supplies the
131
+ * plural and the scope), and the namespace defaulted from the resolved
132
+ * context when the ref carries none. `undefined` when discovery does not
133
+ * serve the kind, where no path exists.
134
+ *
135
+ * This is the observation wire's resolved query address (chant #1620): the
136
+ * per-entity record of what was actually asked of the cluster, so a
137
+ * consumer can tell "looked in the wrong place" from "not there".
138
+ */
139
+ pathFor(ref: ObjectRef, signal?: AbortSignal): Promise<string | undefined>;
128
140
  /** GET one object, returning undefined instead of throwing on a 404. */
129
141
  readIfPresent(ref: ObjectRef, options?: ReadOptions): Promise<K8sObject | undefined>;
130
142
  /** LIST a kind, optionally namespaced and label-filtered. Follows `continue` tokens. */
@@ -153,6 +165,15 @@ export interface K8sClient {
153
165
  * such groupVersion — an answer, not a failure. Cached like `resolve`.
154
166
  */
155
167
  resources(apiVersion: string, signal?: AbortSignal): Promise<ApiResourceInfo[]>;
168
+ /**
169
+ * One groupVersion per API group the cluster serves — each group's own
170
+ * `preferredVersion` (falling back to its first listed version), with the
171
+ * core group's `v1` first (chant #1517). This is the discovery-driven
172
+ * sweep's enumeration unit: one version per group names every kind exactly
173
+ * once, without re-listing the same resources at a second served version.
174
+ * Cached for the client's lifetime, like the rest of discovery.
175
+ */
176
+ preferredGroupVersions(signal?: AbortSignal): Promise<string[]>;
156
177
  /** The API resource lists discovery has been asked for so far, for tests and diagnostics. */
157
178
  discoveryCacheKeys(): string[];
158
179
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAcH,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EACT,SAAS,EAET,gBAAgB,EAEjB,MAAM,SAAS,CAAC;AAEjB,KAAK,UAAU,GAAG,cAAc,yBAAyB,CAAC,CAAC;AAI3D;;;;;;;GAOG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAQ1D;AAED,yEAAyE;AACzE,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO7D;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,GAAG,gBAAgB,CAAM,GACpE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW7B;AAED,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3D,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,kDAAkD;IAClD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAAC;IAChG,8EAA8E;IAC9E,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;;;;;;OAUG;IACH,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3E,wEAAwE;IACxE,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IACrF,wFAAwF;IACxF,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC9E;;;;;;;;OAQG;IACH,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,4DAA4D;IAC5D,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACrE,iFAAiF;IACjF,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,oEAAoE;IACpE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClG;;;;;;OAMG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,6FAA6F;IAC7F,kBAAkB,IAAI,MAAM,EAAE,CAAC;CAChC;AAsBD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAkdxF;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAGpF;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAM/D;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAE9C"}
package/dist/errors.d.ts CHANGED
@@ -30,6 +30,14 @@ export declare class K8sApiError extends Error {
30
30
  /** What was being addressed, e.g. `apps/v1 Deployment prod/web`. */
31
31
  readonly target?: string | undefined;
32
32
  readonly status?: K8sStatus | undefined;
33
+ /**
34
+ * Which cluster the failing read actually talked to, e.g.
35
+ * `context "k3d-fountain-local" (bound by k8s.profiles.local.context)`.
36
+ * Stamped by the client that issued the request (chant #1488) — a
37
+ * `read-failed` that does not name the cluster it read cost an afternoon on
38
+ * a laptop with two k3d clusters, so the failure carries it from birth.
39
+ */
40
+ contextNote?: string;
33
41
  constructor(statusCode: number, reason: string | undefined, apiMessage: string,
34
42
  /** What was being addressed, e.g. `apps/v1 Deployment prod/web`. */
35
43
  target?: string | undefined, status?: K8sStatus | undefined);
@@ -58,6 +66,8 @@ export declare class K8sApiError extends Error {
58
66
  */
59
67
  export declare class K8sTransportError extends Error {
60
68
  readonly target?: string | undefined;
69
+ /** Which cluster context the failed request was aimed at — see {@link K8sApiError.contextNote}. */
70
+ contextNote?: string;
61
71
  constructor(message: string, target?: string | undefined, options?: {
62
72
  cause?: unknown;
63
73
  });
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;aAElB,UAAU,EAAE,MAAM;aAClB,MAAM,EAAE,MAAM,GAAG,SAAS;aAC1B,UAAU,EAAE,MAAM;IAClC,oEAAoE;aACpD,MAAM,CAAC,EAAE,MAAM;aACf,MAAM,CAAC,EAAE,SAAS;gBALlB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,UAAU,EAAE,MAAM;IAClC,oEAAoE;IACpD,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,MAAM,CAAC,EAAE,SAAS,YAAA;IASpC,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,4EAA4E;IAC5E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,8CAA8C;IAC9C,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW;CAapF;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAGxB,MAAM,CAAC,EAAE,MAAM;gBAD/B,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA,EAC/B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAMhC;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,KAAK,CAAC,EAAE,OAAO;CAS5B;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;aAEpC,OAAO,EAAE,MAAM;aACf,OAAO,EAAE,SAAS,MAAM,EAAE;gBAD1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,MAAM,EAAE;CAU7C;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,4EAA4E;AAC5E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;aAE3B,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM,EACpC,OAAO,CAAC,EAAE,MAAM;CAKnB"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;aAWlB,UAAU,EAAE,MAAM;aAClB,MAAM,EAAE,MAAM,GAAG,SAAS;aAC1B,UAAU,EAAE,MAAM;IAClC,oEAAoE;aACpD,MAAM,CAAC,EAAE,MAAM;aACf,MAAM,CAAC,EAAE,SAAS;IAfpC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;gBAGH,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,UAAU,EAAE,MAAM;IAClC,oEAAoE;IACpD,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,MAAM,CAAC,EAAE,SAAS,YAAA;IASpC,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,4EAA4E;IAC5E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,8CAA8C;IAC9C,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW;CAapF;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAMxB,MAAM,CAAC,EAAE,MAAM;IALjC,mGAAmG;IACnG,WAAW,CAAC,EAAE,MAAM,CAAC;gBAGnB,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA,EAC/B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAMhC;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,KAAK,CAAC,EAAE,OAAO;CAS5B;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;aAEpC,OAAO,EAAE,MAAM;aACf,OAAO,EAAE,SAAS,MAAM,EAAE;gBAD1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,MAAM,EAAE;CAU7C;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,4EAA4E;AAC5E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;aAE3B,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM,EACpC,OAAO,CAAC,EAAE,MAAM;CAKnB"}
package/dist/types.d.ts CHANGED
@@ -115,6 +115,15 @@ export interface K8sClientOptions {
115
115
  concurrency?: number;
116
116
  /** Where the context came from, for provenance. Default `ambient`. */
117
117
  contextSource?: "bound" | "ambient";
118
+ /**
119
+ * How the context was selected, in words — e.g.
120
+ * `bound by k8s.profiles.prod.context` or
121
+ * `ambient; no k8s.profiles.local binding`. Stamped (with the resolved
122
+ * context name) onto every API/transport failure the client throws, so a
123
+ * `read-failed` reason names the cluster it read (chant #1488). Defaults to
124
+ * the bare `contextSource`.
125
+ */
126
+ contextLabel?: string;
118
127
  }
119
128
  /**
120
129
  * `@kubernetes/client-node`'s promise-shaped HTTP library: it receives the
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-k8s-client",
3
- "version": "0.42.1",
3
+ "version": "0.44.2",
4
4
  "description": "Typed Kubernetes API client for chant — the read/write path of the k8s lexicon, kept out of the build path",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant/lexicons/k8s/api-client/",
@@ -117,6 +117,47 @@ describe("createK8sClient — kubeconfig and credential policy", () => {
117
117
  expect(layer.requests).toHaveLength(0);
118
118
  });
119
119
 
120
+ test("an API failure names the context it read (#1488)", async () => {
121
+ const layer = cluster({}, (req) =>
122
+ req.path.endsWith("/deployments/web")
123
+ ? { status: 500, body: statusBody(500, "InternalError", "etcdserver: leader changed") }
124
+ : undefined,
125
+ );
126
+ const c = await client(layer);
127
+ const err = await c.read({ apiVersion: "apps/v1", kind: "Deployment", name: "web", namespace: "prod" }).catch((e: unknown) => e);
128
+ expect(err).toBeInstanceOf(K8sApiError);
129
+ expect((err as K8sApiError).contextNote).toBe('context "test-context" (ambient)');
130
+ });
131
+
132
+ test("a bound client's failures carry the binding story via contextLabel (#1488)", async () => {
133
+ const layer = cluster({}, (req) =>
134
+ req.path.endsWith("/deployments/web") ? { status: 503, body: statusBody(503, "ServiceUnavailable", "apiserver down") } : undefined,
135
+ );
136
+ const c = await createK8sClient({
137
+ kubeconfig: fakeKubeconfig({
138
+ contexts: [{ name: "staging-eks" }, { name: "prod-eks", cluster: "prod", user: "prod-user" }],
139
+ currentContext: "staging-eks",
140
+ }),
141
+ context: "prod-eks",
142
+ contextSource: "bound",
143
+ contextLabel: "bound by k8s.profiles.prod.context",
144
+ requestLayer: layer,
145
+ });
146
+ const err = await c.read({ apiVersion: "apps/v1", kind: "Deployment", name: "web", namespace: "prod" }).catch((e: unknown) => e);
147
+ expect(err).toBeInstanceOf(K8sApiError);
148
+ expect((err as K8sApiError).contextNote).toBe('context "prod-eks" (bound by k8s.profiles.prod.context)');
149
+ });
150
+
151
+ test("a transport failure names the context it was aimed at (#1488)", async () => {
152
+ const layer = fakeRequestLayer(() => {
153
+ throw new Error("connect ECONNREFUSED 127.0.0.1:6443");
154
+ });
155
+ const c = await createK8sClient({ kubeconfig: fakeKubeconfig(), requestLayer: layer });
156
+ const err = await c.read({ apiVersion: "apps/v1", kind: "Deployment", name: "web" }).catch((e: unknown) => e);
157
+ expect(err).toBeInstanceOf(K8sTransportError);
158
+ expect((err as K8sTransportError).contextNote).toBe('context "test-context" (ambient)');
159
+ });
160
+
120
161
  test("an exec credential plugin off the allowlist refuses before it can run", async () => {
121
162
  const layer = cluster();
122
163
  await expect(
@@ -201,6 +242,39 @@ describe("resource resolution through the cluster's own discovery", () => {
201
242
  expect(c.discoveryCacheKeys()).toEqual(["apps/v1"]);
202
243
  });
203
244
 
245
+ // chant #1517 — the runtime-children sweep's enumeration unit: one version
246
+ // per group, so every kind is named exactly once.
247
+ test("preferredGroupVersions returns core v1 plus each group's own preferredVersion, once", async () => {
248
+ const layer = fakeRequestLayer((req) => {
249
+ if (req.path === "/api") return { body: { kind: "APIVersions", versions: ["v1"] } };
250
+ if (req.path === "/apis") {
251
+ return {
252
+ body: {
253
+ kind: "APIGroupList",
254
+ groups: [
255
+ {
256
+ name: "apps",
257
+ preferredVersion: { groupVersion: "apps/v1", version: "v1" },
258
+ versions: [{ groupVersion: "apps/v1" }, { groupVersion: "apps/v1beta1" }],
259
+ },
260
+ // A group reporting no preferredVersion falls back to its first
261
+ // listed version rather than being dropped.
262
+ { name: "widgets.example.com", versions: [{ groupVersion: "widgets.example.com/v1alpha1" }] },
263
+ ],
264
+ },
265
+ };
266
+ }
267
+ return { status: 404, body: statusBody(404, "NotFound", `${req.path} not found`) };
268
+ });
269
+ const c = await client(layer);
270
+
271
+ expect(await c.preferredGroupVersions()).toEqual(["v1", "apps/v1", "widgets.example.com/v1alpha1"]);
272
+
273
+ // Cached: a second call issues no further discovery requests.
274
+ await c.preferredGroupVersions();
275
+ expect(layer.paths().filter((p) => p === "/apis")).toHaveLength(1);
276
+ });
277
+
204
278
  test("kubectl-style resource strings resolve the way kubectl resolves them", async () => {
205
279
  const c = await client(cluster());
206
280
  // plural.group — how waitForReady's callers have always spelled CRDs
@@ -304,6 +378,28 @@ describe("reads", () => {
304
378
  c.readIfPresent({ apiVersion: "apps/v1", kind: "Deployment", name: "denied", namespace: "prod" }),
305
379
  ).rejects.toThrow(K8sApiError);
306
380
  });
381
+
382
+ test("pathFor returns the exact path read would GET, with the namespace defaulted (chant #1620)", async () => {
383
+ const c = await client(cluster());
384
+ // No namespace on the ref: the resolved path shows where the read actually
385
+ // went — the context's default — which is the whole diagnostic point.
386
+ expect(await c.pathFor({ apiVersion: "apps/v1", kind: "Deployment", name: "web" })).toBe(
387
+ "/apis/apps/v1/namespaces/default/deployments/web",
388
+ );
389
+ expect(await c.pathFor({ apiVersion: "apps/v1", kind: "Deployment", name: "web", namespace: "prod" })).toBe(
390
+ "/apis/apps/v1/namespaces/prod/deployments/web",
391
+ );
392
+ });
393
+
394
+ test("pathFor on a cluster-scoped kind carries no namespace segment", async () => {
395
+ const c = await client(cluster());
396
+ expect(await c.pathFor({ apiVersion: "v1", kind: "Namespace", name: "ns-a" })).toBe("/api/v1/namespaces/ns-a");
397
+ });
398
+
399
+ test("pathFor is undefined for a kind discovery does not serve — no path exists", async () => {
400
+ const c = await client(cluster());
401
+ expect(await c.pathFor({ apiVersion: "widgets.example.com/v1", kind: "Widget", name: "w" })).toBeUndefined();
402
+ });
307
403
  });
308
404
 
309
405
  describe("concurrency", () => {
package/src/client.ts CHANGED
@@ -187,6 +187,18 @@ export interface K8sClient {
187
187
  resolve(selector: ResourceSelector, signal?: AbortSignal): Promise<ApiResourceInfo | undefined>;
188
188
  /** GET one object. Throws {@link K8sApiError} with `notFound` when absent. */
189
189
  read(ref: ObjectRef, options?: ReadOptions): Promise<K8sObject>;
190
+ /**
191
+ * The exact request path {@link read} would GET for this ref — apiVersion and
192
+ * kind resolved against the cluster's own discovery (which supplies the
193
+ * plural and the scope), and the namespace defaulted from the resolved
194
+ * context when the ref carries none. `undefined` when discovery does not
195
+ * serve the kind, where no path exists.
196
+ *
197
+ * This is the observation wire's resolved query address (chant #1620): the
198
+ * per-entity record of what was actually asked of the cluster, so a
199
+ * consumer can tell "looked in the wrong place" from "not there".
200
+ */
201
+ pathFor(ref: ObjectRef, signal?: AbortSignal): Promise<string | undefined>;
190
202
  /** GET one object, returning undefined instead of throwing on a 404. */
191
203
  readIfPresent(ref: ObjectRef, options?: ReadOptions): Promise<K8sObject | undefined>;
192
204
  /** LIST a kind, optionally namespaced and label-filtered. Follows `continue` tokens. */
@@ -215,6 +227,15 @@ export interface K8sClient {
215
227
  * such groupVersion — an answer, not a failure. Cached like `resolve`.
216
228
  */
217
229
  resources(apiVersion: string, signal?: AbortSignal): Promise<ApiResourceInfo[]>;
230
+ /**
231
+ * One groupVersion per API group the cluster serves — each group's own
232
+ * `preferredVersion` (falling back to its first listed version), with the
233
+ * core group's `v1` first (chant #1517). This is the discovery-driven
234
+ * sweep's enumeration unit: one version per group names every kind exactly
235
+ * once, without re-listing the same resources at a second served version.
236
+ * Cached for the client's lifetime, like the rest of discovery.
237
+ */
238
+ preferredGroupVersions(signal?: AbortSignal): Promise<string[]>;
218
239
  /** The API resource lists discovery has been asked for so far, for tests and diagnostics. */
219
240
  discoveryCacheKeys(): string[];
220
241
  }
@@ -231,6 +252,14 @@ interface ApiResourceListResponse {
231
252
  }>;
232
253
  }
233
254
 
255
+ /** The `/api` + `/apis` root discovery documents, kept structured so both the
256
+ * flat enumeration (`servedGroupVersions`) and the one-per-group enumeration
257
+ * (`preferredGroupVersions`, chant #1517) derive from a single cached fetch. */
258
+ interface RootDiscovery {
259
+ coreVersions: string[];
260
+ groups: Array<{ preferred?: string; versions: string[] }>;
261
+ }
262
+
234
263
  /**
235
264
  * Build a client. Nothing is read from the network here — the kubeconfig is
236
265
  * parsed, the context resolved, and the credential policy enforced, all before
@@ -301,12 +330,23 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
301
330
  const defaultNamespace = kc.getContextObject(kc.getCurrentContext())?.namespace || "default";
302
331
  const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
303
332
 
333
+ // Which cluster this client reads, in words, stamped onto every failure it
334
+ // throws (chant #1488). A `read-failed` that does not say which context it
335
+ // read turned "the ambient context moved" into an afternoon of debugging;
336
+ // naming it here — at construction, once — means every read path's
337
+ // classification carries it without each caller remembering to.
338
+ const contextNote = `context "${provenance.context ?? "(unset)"}" (${options.contextLabel ?? provenance.contextSource})`;
339
+ function noted<T extends { contextNote?: string }>(err: T): T {
340
+ err.contextNote = contextNote;
341
+ return err;
342
+ }
343
+
304
344
  // apiVersion → its APIResourceList, or null when the cluster does not serve
305
345
  // that group/version at all. Promises are cached, not values, so N entities
306
346
  // resolved concurrently issue one discovery request between them rather
307
347
  // than N identical ones.
308
348
  const discoveryCache = new Map<string, Promise<ApiResourceListResponse | null>>();
309
- let groupVersionsCache: Promise<string[]> | undefined;
349
+ let rootDiscoveryCache: Promise<RootDiscovery> | undefined;
310
350
 
311
351
  async function send(
312
352
  path: string,
@@ -338,10 +378,10 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
338
378
  .send(ctx)
339
379
  .toPromise()) as unknown as ResponseContextLike;
340
380
  } catch (err) {
341
- throw new K8sTransportError(
342
- err instanceof Error ? err.message : String(err),
343
- opts.target ?? `${method} ${path}`,
344
- { cause: err },
381
+ throw noted(
382
+ new K8sTransportError(err instanceof Error ? err.message : String(err), opts.target ?? `${method} ${path}`, {
383
+ cause: err,
384
+ }),
345
385
  );
346
386
  }
347
387
 
@@ -349,10 +389,12 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
349
389
  try {
350
390
  text = await response.body.text();
351
391
  } catch (err) {
352
- throw new K8sTransportError(
353
- `response body could not be read: ${err instanceof Error ? err.message : String(err)}`,
354
- opts.target ?? `${method} ${path}`,
355
- { cause: err },
392
+ throw noted(
393
+ new K8sTransportError(
394
+ `response body could not be read: ${err instanceof Error ? err.message : String(err)}`,
395
+ opts.target ?? `${method} ${path}`,
396
+ { cause: err },
397
+ ),
356
398
  );
357
399
  }
358
400
  return { status: response.httpStatusCode, body: text };
@@ -365,15 +407,17 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
365
407
  ): Promise<T> {
366
408
  const { status, body } = await send(path, method, opts);
367
409
  if (status < 200 || status > 299) {
368
- throw K8sApiError.fromResponse(status, body, opts.target);
410
+ throw noted(K8sApiError.fromResponse(status, body, opts.target));
369
411
  }
370
412
  try {
371
413
  return JSON.parse(body) as T;
372
414
  } catch (err) {
373
- throw new K8sTransportError(
374
- `the API server returned HTTP ${status} with a body that is not JSON`,
375
- opts.target ?? `${method} ${path}`,
376
- { cause: err },
415
+ throw noted(
416
+ new K8sTransportError(
417
+ `the API server returned HTTP ${status} with a body that is not JSON`,
418
+ opts.target ?? `${method} ${path}`,
419
+ { cause: err },
420
+ ),
377
421
  );
378
422
  }
379
423
  }
@@ -399,15 +443,13 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
399
443
  return pending;
400
444
  }
401
445
 
402
- async function servedGroupVersions(signal?: AbortSignal): Promise<string[]> {
403
- if (groupVersionsCache) return groupVersionsCache;
404
- groupVersionsCache = (async () => {
405
- const out: string[] = [];
446
+ async function rootDiscovery(signal?: AbortSignal): Promise<RootDiscovery> {
447
+ if (rootDiscoveryCache) return rootDiscoveryCache;
448
+ rootDiscoveryCache = (async (): Promise<RootDiscovery> => {
406
449
  const core = await sendJson<{ versions?: string[] }>("/api", "GET", {
407
450
  signal,
408
451
  target: "discovery /api",
409
452
  });
410
- out.push(...(core.versions ?? ["v1"]));
411
453
  const groups = await sendJson<{
412
454
  groups?: Array<{
413
455
  name?: string;
@@ -415,19 +457,43 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
415
457
  versions?: Array<{ groupVersion?: string }>;
416
458
  }>;
417
459
  }>("/apis", "GET", { signal, target: "discovery /apis" });
418
- for (const group of groups.groups ?? []) {
419
- const preferred = group.preferredVersion?.groupVersion;
420
- if (preferred) out.push(preferred);
421
- for (const v of group.versions ?? []) {
422
- if (v.groupVersion && v.groupVersion !== preferred) out.push(v.groupVersion);
423
- }
424
- }
425
- return [...new Set(out)];
460
+ return {
461
+ coreVersions: core.versions ?? ["v1"],
462
+ groups: (groups.groups ?? []).map((group) => ({
463
+ preferred: group.preferredVersion?.groupVersion,
464
+ versions: (group.versions ?? [])
465
+ .map((v) => v.groupVersion)
466
+ .filter((gv): gv is string => typeof gv === "string" && gv.length > 0),
467
+ })),
468
+ };
426
469
  })().catch((err: unknown) => {
427
- groupVersionsCache = undefined;
470
+ rootDiscoveryCache = undefined;
428
471
  throw err;
429
472
  });
430
- return groupVersionsCache;
473
+ return rootDiscoveryCache;
474
+ }
475
+
476
+ async function servedGroupVersions(signal?: AbortSignal): Promise<string[]> {
477
+ const root = await rootDiscovery(signal);
478
+ const out: string[] = [...root.coreVersions];
479
+ for (const group of root.groups) {
480
+ if (group.preferred) out.push(group.preferred);
481
+ for (const gv of group.versions) {
482
+ if (gv !== group.preferred) out.push(gv);
483
+ }
484
+ }
485
+ return [...new Set(out)];
486
+ }
487
+
488
+ async function preferredGroupVersions(signal?: AbortSignal): Promise<string[]> {
489
+ const root = await rootDiscovery(signal);
490
+ const core = root.coreVersions.includes("v1") ? "v1" : root.coreVersions[0];
491
+ const out: string[] = core ? [core] : [];
492
+ for (const group of root.groups) {
493
+ const gv = group.preferred ?? group.versions[0];
494
+ if (gv) out.push(gv);
495
+ }
496
+ return [...new Set(out)];
431
497
  }
432
498
 
433
499
  function toInfo(apiVersion: string, entry: NonNullable<ApiResourceListResponse["resources"]>[number]): ApiResourceInfo {
@@ -518,6 +584,12 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
518
584
  return parts.join("/");
519
585
  }
520
586
 
587
+ async function pathFor(ref: ObjectRef, signal?: AbortSignal): Promise<string | undefined> {
588
+ const info = await resolve({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
589
+ if (!info) return undefined;
590
+ return objectPath(info, ref.name, ref.namespace);
591
+ }
592
+
521
593
  async function read(ref: ObjectRef, opts: ReadOptions = {}): Promise<K8sObject> {
522
594
  const info = await resolveOrThrow({ apiVersion: ref.apiVersion, kind: ref.kind }, opts.signal);
523
595
  return sendJson<K8sObject>(objectPath(info, ref.name, ref.namespace), "GET", {
@@ -618,7 +690,7 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
618
690
  target,
619
691
  });
620
692
  if (status < 200 || status > 299) {
621
- throw K8sApiError.fromResponse(status, body, target);
693
+ throw noted(K8sApiError.fromResponse(status, body, target));
622
694
  }
623
695
  return body;
624
696
  }
@@ -648,6 +720,7 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
648
720
  defaultNamespace,
649
721
  resolve,
650
722
  read,
723
+ pathFor,
651
724
  readIfPresent,
652
725
  list,
653
726
  readLog,
@@ -655,6 +728,7 @@ export async function createK8sClient(options: K8sClientOptions = {}): Promise<K
655
728
  delete: remove,
656
729
  concurrently: (items, fn) => mapConcurrent(items, fn, concurrency),
657
730
  resources: resourcesOf,
731
+ preferredGroupVersions,
658
732
  discoveryCacheKeys: () => [...discoveryCache.keys()].sort(),
659
733
  };
660
734
  }
package/src/errors.ts CHANGED
@@ -26,6 +26,15 @@ export interface K8sStatus {
26
26
  * `reason` come from the response, not from parsing text.
27
27
  */
28
28
  export class K8sApiError extends Error {
29
+ /**
30
+ * Which cluster the failing read actually talked to, e.g.
31
+ * `context "k3d-fountain-local" (bound by k8s.profiles.local.context)`.
32
+ * Stamped by the client that issued the request (chant #1488) — a
33
+ * `read-failed` that does not name the cluster it read cost an afternoon on
34
+ * a laptop with two k3d clusters, so the failure carries it from birth.
35
+ */
36
+ contextNote?: string;
37
+
29
38
  constructor(
30
39
  public readonly statusCode: number,
31
40
  public readonly reason: string | undefined,
@@ -90,6 +99,9 @@ export class K8sApiError extends Error {
90
99
  * connect" and "the server said no" are different observations.
91
100
  */
92
101
  export class K8sTransportError extends Error {
102
+ /** Which cluster context the failed request was aimed at — see {@link K8sApiError.contextNote}. */
103
+ contextNote?: string;
104
+
93
105
  constructor(
94
106
  message: string,
95
107
  public readonly target?: string,
package/src/types.ts CHANGED
@@ -125,6 +125,15 @@ export interface K8sClientOptions {
125
125
  concurrency?: number;
126
126
  /** Where the context came from, for provenance. Default `ambient`. */
127
127
  contextSource?: "bound" | "ambient";
128
+ /**
129
+ * How the context was selected, in words — e.g.
130
+ * `bound by k8s.profiles.prod.context` or
131
+ * `ambient; no k8s.profiles.local binding`. Stamped (with the resolved
132
+ * context name) onto every API/transport failure the client throws, so a
133
+ * `read-failed` reason names the cluster it read (chant #1488). Defaults to
134
+ * the bare `contextSource`.
135
+ */
136
+ contextLabel?: string;
128
137
  }
129
138
 
130
139
  /**