@intentius/chant-k8s-client 0.44.3 → 0.44.5

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
@@ -34,7 +34,7 @@
34
34
  * path that writes the `Authorization` header — runs for real, which is what
35
35
  * makes a test that injects one worth writing.
36
36
  */
37
- import type { ApiResourceInfo, ClientProvenance, K8sClientOptions, K8sObject, ObjectRef, ResourceSelector } from "./types.js";
37
+ import type { ApiResourceInfo, ClientProvenance, K8sClientOptions, K8sObject, ObjectRef, ResourceSelector, KubeconfigView } from "./types.js";
38
38
  type ClientNode = typeof import("@kubernetes/client-node");
39
39
  /**
40
40
  * Load `@kubernetes/client-node`, once per process.
@@ -58,6 +58,31 @@ export declare function isK8sClientAvailable(): Promise<boolean>;
58
58
  * kubeconfig can be read at all.
59
59
  */
60
60
  export declare function readAmbientContext(options?: Pick<K8sClientOptions, "kubeconfig" | "kubeconfigPath">): Promise<string | undefined>;
61
+ /**
62
+ * The merged kubeconfig's contexts and their apiservers, without building a
63
+ * client (chant #1630).
64
+ *
65
+ * A lens showing an operator which cluster the k8s half is being read from
66
+ * needs an address out of a file, not a client about to talk to one. Going
67
+ * through `createK8sClient` for that answer refuses in two places it should
68
+ * not: an auth plugin outside the exec allowlist throws, so a cluster whose
69
+ * address is sitting right there in the kubeconfig — and which kubectl reads
70
+ * without complaint — reports as no target at all; and a context resolving to
71
+ * no cluster throws where the caller wants "nothing to report". Both refusals
72
+ * are right for a client that is about to send a request and wrong for a
73
+ * config read. Enumerating also takes one client per context, and enumerating
74
+ * is exactly what a lens does.
75
+ *
76
+ * So: no credential touched, no exec plugin run, no request issued. Every
77
+ * failure — no kubeconfig, an unreadable one, `@kubernetes/client-node` not
78
+ * installed — is an empty result rather than a throw, because a lens on an
79
+ * aws-only project running on a machine that has never seen Kubernetes has
80
+ * nothing to report and nothing to fail about.
81
+ *
82
+ * `server` per context rather than a separate cluster map: joining the two is
83
+ * what every caller does anyway.
84
+ */
85
+ export declare function readKubeconfigView(options?: Pick<K8sClientOptions, "kubeconfig" | "kubeconfigPath">): Promise<KubeconfigView>;
61
86
  /** Options for a single object read. */
62
87
  export interface ReadOptions {
63
88
  signal?: AbortSignal;
@@ -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":"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"}
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * `examples/k8s-client-boundary.test.ts` walks the static import graph to prove
12
12
  * it stays that way.
13
13
  */
14
- export { createK8sClient, readAmbientContext, loadClientNode, isK8sClientAvailable, apiVersionPath, splitApiVersion, selectorText, refText, } from "./client.js";
14
+ export { createK8sClient, readAmbientContext, readKubeconfigView, loadClientNode, isK8sClientAvailable, apiVersionPath, splitApiVersion, selectorText, refText, } from "./client.js";
15
15
  export type { K8sClient, ReadOptions, ApplyOptions, DeleteOptions, ListOptions, ReadLogOptions } from "./client.js";
16
16
  export { K8sApiError, K8sTransportError, K8sClientUnavailableError, ExecCredentialNotAllowedError, FieldManagerError, KubeConfigError, UnknownResourceError, } from "./errors.js";
17
17
  export type { K8sStatus } from "./errors.js";
@@ -24,5 +24,5 @@ export type { ManagedFieldsEntry, ManagerFieldSet } from "./managed-fields.js";
24
24
  export { DEFAULT_EXEC_ALLOWLIST, assertExecCredentialAllowed, credentialPathOf, execConfigOf, execCommandName, } from "./credentials.js";
25
25
  export type { ExecConfig, KubeConfigUser } from "./credentials.js";
26
26
  export { mapConcurrent, DEFAULT_CONCURRENCY } from "./concurrency.js";
27
- export type { ApiResourceInfo, ClientProvenance, CredentialPath, K8sClientOptions, K8sObject, ObjectRef, RequestContextLike, RequestLayer, ResourceSelector, ResponseContextLike, } from "./types.js";
27
+ export type { ApiResourceInfo, ClientProvenance, CredentialPath, K8sClientOptions, KubeconfigContextInfo, KubeconfigView, K8sObject, ObjectRef, RequestContextLike, RequestLayer, ResourceSelector, ResponseContextLike, } from "./types.js";
28
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,OAAO,GACR,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEjH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,yBAAyB,EACzB,6BAA6B,EAC7B,iBAAiB,EACjB,eAAe,EACf,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,EACL,eAAe,EACf,WAAW,EACX,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEnE,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,OAAO,GACR,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEjH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,yBAAyB,EACzB,6BAA6B,EAC7B,iBAAiB,EACjB,eAAe,EACf,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,EACL,eAAe,EACf,WAAW,EACX,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEnE,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,SAAS,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Loading the kubeconfig the way kubectl loads it — chant #1630.
3
+ *
4
+ * `@kubernetes/client-node` parses a kubeconfig faithfully and merges a
5
+ * `KUBECONFIG` path list on two rules that are not kubectl's. Both are
6
+ * reproducible with two files and no cluster (checked against client-node
7
+ * 1.4.0), and both make chant bind a different cluster than the `kubectl get`
8
+ * the operator ran to check:
9
+ *
10
+ * - **Duplicate names throw.** `mergeConfig` funnels every entry through
11
+ * `addCluster`/`addUser`/`addContext`, each of which throws `Duplicate
12
+ * cluster: <name>` on a name it already holds. kubectl's documented rule is
13
+ * the opposite — keep the first, ignore later ones, never fail. Overlapping
14
+ * names across a list are ordinary: a team-wide file plus a personal one
15
+ * both naming `dev` is the usual way people get there. Today that throw is
16
+ * swallowed by `readAmbientContext`'s try/catch, so chant reads those users
17
+ * as having no kubeconfig at all.
18
+ * - **current-context is last-wins.** `loadFromDefault(opts,
19
+ * contextFromStartingConfig)` defaults the second argument to `false`, and
20
+ * `mergeConfig` then overwrites `currentContext` from each later file.
21
+ * kubectl takes the FIRST file that sets one.
22
+ *
23
+ * So the multi-file case is merged here instead: one `KubeConfig` per file
24
+ * (which is also what keeps each file's relative certificate paths resolved
25
+ * against its own directory), then a first-wins union by name and the first
26
+ * `current-context` that appears. Everything else — the single-file path, the
27
+ * `~/.kube/config` fallback, in-cluster credentials — stays client-node's
28
+ * `loadFromDefault`, with the one-word fix that gives it kubectl's
29
+ * current-context.
30
+ *
31
+ * A file in the list that cannot be read is skipped rather than fatal, which
32
+ * is also kubectl's rule: a stale entry in a shared `KUBECONFIG` export must
33
+ * not take the whole config down with it.
34
+ */
35
+ import type * as k8s from "@kubernetes/client-node";
36
+ type ClientNode = typeof import("@kubernetes/client-node");
37
+ /** Where the loaded kubeconfig came from, for {@link ClientProvenance}. */
38
+ export type KubeconfigSource = "explicit-string" | "explicit-path" | "default" | "in-cluster";
39
+ export interface LoadedKubeConfig {
40
+ kc: k8s.KubeConfig;
41
+ source: KubeconfigSource;
42
+ }
43
+ /**
44
+ * The kubeconfig this invocation should read, by the same precedence every
45
+ * entry point in this package uses: an explicit literal, then an explicit
46
+ * path, then the ambient default (`KUBECONFIG`, `~/.kube/config`, in-cluster).
47
+ *
48
+ * Throws whatever client-node throws for an explicit string or path — those
49
+ * are the caller naming a file, and a caller who names an unreadable one wants
50
+ * to hear about it. The ambient path never throws for a duplicate name.
51
+ */
52
+ export declare function loadKubeConfig(mod: ClientNode, options?: {
53
+ kubeconfig?: string;
54
+ kubeconfigPath?: string;
55
+ }): LoadedKubeConfig;
56
+ export {};
57
+ //# sourceMappingURL=kubeconfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kubeconfig.d.ts","sourceRoot":"","sources":["../src/kubeconfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,KAAK,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAEpD,KAAK,UAAU,GAAG,cAAc,yBAAyB,CAAC,CAAC;AAE3D,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,eAAe,GAAG,SAAS,GAAG,YAAY,CAAC;AAE9F,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,GAAG,CAAC,UAAU,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAwDD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,UAAU,EACf,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7D,gBAAgB,CAqBlB"}
package/dist/types.d.ts CHANGED
@@ -85,6 +85,25 @@ export interface ClientProvenance {
85
85
  /** Where the kubeconfig itself came from. */
86
86
  kubeconfigSource: "explicit-string" | "explicit-path" | "default" | "in-cluster";
87
87
  }
88
+ /**
89
+ * One context of the merged kubeconfig, joined to the apiserver its cluster
90
+ * resolves to (chant #1630). Plain data read out of a file: no credential is
91
+ * touched, no exec plugin runs, no request is issued.
92
+ */
93
+ export interface KubeconfigContextInfo {
94
+ name: string;
95
+ /** Cluster name the context names. Empty when the context declares none. */
96
+ cluster: string;
97
+ user?: string;
98
+ namespace?: string;
99
+ /** The apiserver URL the named cluster resolves to, when the config has one. */
100
+ server?: string;
101
+ }
102
+ /** What {@link import("./client.js").readKubeconfigView} returns. */
103
+ export interface KubeconfigView {
104
+ contexts: KubeconfigContextInfo[];
105
+ currentContext?: string;
106
+ }
88
107
  /** Options for {@link import("./client.js").createK8sClient}. */
89
108
  export interface K8sClientOptions {
90
109
  /**
@@ -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.44.3",
3
+ "version": "0.44.5",
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/",
package/src/client.ts CHANGED
@@ -47,6 +47,7 @@ import { assertExecCredentialAllowed, credentialPathOf, DEFAULT_EXEC_ALLOWLIST }
47
47
  import { asFieldManagerConflict } from "./conflict";
48
48
  import { assertValidFieldManager, CHANT_FIELD_MANAGER } from "./field-manager";
49
49
  import { DEFAULT_CONCURRENCY, mapConcurrent } from "./concurrency";
50
+ import { loadKubeConfig } from "./kubeconfig";
50
51
  import type {
51
52
  ApiResourceInfo,
52
53
  ClientProvenance,
@@ -56,6 +57,8 @@ import type {
56
57
  RequestContextLike,
57
58
  ResourceSelector,
58
59
  ResponseContextLike,
60
+ KubeconfigContextInfo,
61
+ KubeconfigView,
59
62
  } from "./types";
60
63
 
61
64
  type ClientNode = typeof import("@kubernetes/client-node");
@@ -103,16 +106,64 @@ export async function isK8sClientAvailable(): Promise<boolean> {
103
106
  export async function readAmbientContext(
104
107
  options: Pick<K8sClientOptions, "kubeconfig" | "kubeconfigPath"> = {},
105
108
  ): Promise<string | undefined> {
106
- const mod = await loadClientNode();
107
- const kc = new mod.KubeConfig();
108
109
  try {
109
- if (options.kubeconfig !== undefined) kc.loadFromString(options.kubeconfig);
110
- else if (options.kubeconfigPath !== undefined) kc.loadFromFile(options.kubeconfigPath);
111
- else kc.loadFromDefault();
110
+ const { kc } = loadKubeConfig(await loadClientNode(), options);
111
+ return kc.getCurrentContext() || undefined;
112
112
  } catch {
113
113
  return undefined;
114
114
  }
115
- return kc.getCurrentContext() || undefined;
115
+ }
116
+
117
+ /**
118
+ * The merged kubeconfig's contexts and their apiservers, without building a
119
+ * client (chant #1630).
120
+ *
121
+ * A lens showing an operator which cluster the k8s half is being read from
122
+ * needs an address out of a file, not a client about to talk to one. Going
123
+ * through `createK8sClient` for that answer refuses in two places it should
124
+ * not: an auth plugin outside the exec allowlist throws, so a cluster whose
125
+ * address is sitting right there in the kubeconfig — and which kubectl reads
126
+ * without complaint — reports as no target at all; and a context resolving to
127
+ * no cluster throws where the caller wants "nothing to report". Both refusals
128
+ * are right for a client that is about to send a request and wrong for a
129
+ * config read. Enumerating also takes one client per context, and enumerating
130
+ * is exactly what a lens does.
131
+ *
132
+ * So: no credential touched, no exec plugin run, no request issued. Every
133
+ * failure — no kubeconfig, an unreadable one, `@kubernetes/client-node` not
134
+ * installed — is an empty result rather than a throw, because a lens on an
135
+ * aws-only project running on a machine that has never seen Kubernetes has
136
+ * nothing to report and nothing to fail about.
137
+ *
138
+ * `server` per context rather than a separate cluster map: joining the two is
139
+ * what every caller does anyway.
140
+ */
141
+ export async function readKubeconfigView(
142
+ options: Pick<K8sClientOptions, "kubeconfig" | "kubeconfigPath"> = {},
143
+ ): Promise<KubeconfigView> {
144
+ let kc;
145
+ try {
146
+ ({ kc } = loadKubeConfig(await loadClientNode(), options));
147
+ } catch {
148
+ return { contexts: [] };
149
+ }
150
+ try {
151
+ const servers = new Map<string, string | undefined>();
152
+ for (const cluster of kc.getClusters()) servers.set(cluster.name, cluster.server || undefined);
153
+ const contexts: KubeconfigContextInfo[] = kc.getContexts().map((context) => {
154
+ const server = servers.get(context.cluster);
155
+ return {
156
+ name: context.name,
157
+ cluster: context.cluster ?? "",
158
+ ...(context.user ? { user: context.user } : {}),
159
+ ...(context.namespace ? { namespace: context.namespace } : {}),
160
+ ...(server ? { server } : {}),
161
+ };
162
+ });
163
+ return { contexts, ...(kc.getCurrentContext() ? { currentContext: kc.getCurrentContext() } : {}) };
164
+ } catch {
165
+ return { contexts: [] };
166
+ }
116
167
  }
117
168
 
118
169
  /** Options for a single object read. */
@@ -268,18 +319,10 @@ interface RootDiscovery {
268
319
  export async function createK8sClient(options: K8sClientOptions = {}): Promise<K8sClient> {
269
320
  const mod = await loadClientNode();
270
321
 
271
- const kc = new mod.KubeConfig();
272
- let kubeconfigSource: ClientProvenance["kubeconfigSource"];
273
- if (options.kubeconfig !== undefined) {
274
- kc.loadFromString(options.kubeconfig);
275
- kubeconfigSource = "explicit-string";
276
- } else if (options.kubeconfigPath !== undefined) {
277
- kc.loadFromFile(options.kubeconfigPath);
278
- kubeconfigSource = "explicit-path";
279
- } else {
280
- kc.loadFromDefault();
281
- kubeconfigSource = kc.getCurrentContext() === "inCluster" ? "in-cluster" : "default";
282
- }
322
+ // kubectl's merge semantics, not client-node's (#1630): a KUBECONFIG list
323
+ // with overlapping names resolves first-wins instead of throwing, and
324
+ // current-context comes from the first file that sets one.
325
+ const { kc, source: kubeconfigSource } = loadKubeConfig(mod, options);
283
326
 
284
327
  if (options.context !== undefined) {
285
328
  if (!kc.getContextObject(options.context)) {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  export {
16
16
  createK8sClient,
17
17
  readAmbientContext,
18
+ readKubeconfigView,
18
19
  loadClientNode,
19
20
  isK8sClientAvailable,
20
21
  apiVersionPath,
@@ -85,6 +86,8 @@ export type {
85
86
  ClientProvenance,
86
87
  CredentialPath,
87
88
  K8sClientOptions,
89
+ KubeconfigContextInfo,
90
+ KubeconfigView,
88
91
  K8sObject,
89
92
  ObjectRef,
90
93
  RequestContextLike,
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The kubeconfig read surface and kubectl's merge semantics — chant #1630.
3
+ *
4
+ * Every case writes its own kubeconfig files into a temp directory and points
5
+ * `KUBECONFIG` at them, so nothing reads the developer's real config and
6
+ * nothing contacts a cluster: these are file reads, and the point of the
7
+ * surface under test is that they stay file reads.
8
+ *
9
+ * Where kubectl is on PATH, `kubectl config view` is asked the same questions
10
+ * and its answers are asserted to match — the merge rules are only worth
11
+ * having because they are kubectl's, and a hardcoded expectation cannot prove
12
+ * that. Skipped (with the expectations still asserted on their own) where it
13
+ * is not installed.
14
+ */
15
+
16
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
17
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
18
+ import { spawnSync } from "node:child_process";
19
+ import { tmpdir } from "node:os";
20
+ import { delimiter, join } from "node:path";
21
+ import { readAmbientContext, readKubeconfigView, createK8sClient } from "./client";
22
+ import { fakeKubeconfig, fakeRequestLayer } from "./testing";
23
+
24
+ /** A kubeconfig naming one cluster, one user and one context, with a chosen
25
+ * current-context — the material both merge divergences are reproduced with. */
26
+ function configYaml(opts: { cluster: string; server: string; context: string; user?: string }): string {
27
+ const user = opts.user ?? `user-${opts.context}`;
28
+ return [
29
+ "apiVersion: v1",
30
+ "kind: Config",
31
+ `current-context: ${opts.context}`,
32
+ "clusters:",
33
+ ` - name: ${opts.cluster}`,
34
+ " cluster:",
35
+ ` server: ${opts.server}`,
36
+ " insecure-skip-tls-verify: true",
37
+ "users:",
38
+ ` - name: ${user}`,
39
+ " user:",
40
+ ' token: "test-token"',
41
+ "contexts:",
42
+ ` - name: ${opts.context}`,
43
+ " context:",
44
+ ` cluster: ${opts.cluster}`,
45
+ ` user: ${user}`,
46
+ "",
47
+ ].join("\n");
48
+ }
49
+
50
+ const kubectlAvailable = spawnSync("kubectl", ["version", "--client=true"], { encoding: "utf8" }).status === 0;
51
+
52
+ /** `kubectl config view` against a KUBECONFIG list, or undefined without kubectl. */
53
+ function kubectlView(kubeconfig: string, args: string[]): string | undefined {
54
+ if (!kubectlAvailable) return undefined;
55
+ const r = spawnSync("kubectl", ["config", ...args], {
56
+ encoding: "utf8",
57
+ env: { ...process.env, KUBECONFIG: kubeconfig },
58
+ });
59
+ return r.status === 0 ? r.stdout.trim() : undefined;
60
+ }
61
+
62
+ let dir: string;
63
+ let savedKubeconfig: string | undefined;
64
+
65
+ beforeEach(() => {
66
+ dir = mkdtempSync(join(tmpdir(), "chant-kubeconfig-"));
67
+ savedKubeconfig = process.env.KUBECONFIG;
68
+ });
69
+
70
+ afterEach(() => {
71
+ if (savedKubeconfig === undefined) delete process.env.KUBECONFIG;
72
+ else process.env.KUBECONFIG = savedKubeconfig;
73
+ rmSync(dir, { recursive: true, force: true });
74
+ });
75
+
76
+ function write(name: string, content: string): string {
77
+ const path = join(dir, name);
78
+ writeFileSync(path, content);
79
+ return path;
80
+ }
81
+
82
+ describe("readKubeconfigView (#1630)", () => {
83
+ test("returns each context joined to its apiserver, plus the current context", async () => {
84
+ const view = await readKubeconfigView({
85
+ kubeconfig: fakeKubeconfig({
86
+ contexts: [
87
+ { name: "ctx-a", cluster: "cluster-a", user: "user-a", namespace: "team" },
88
+ { name: "ctx-b", cluster: "cluster-b", user: "user-b" },
89
+ ],
90
+ currentContext: "ctx-b",
91
+ server: "https://cluster.test:6443",
92
+ }),
93
+ });
94
+
95
+ expect(view.currentContext).toBe("ctx-b");
96
+ expect(view.contexts).toEqual([
97
+ { name: "ctx-a", cluster: "cluster-a", user: "user-a", namespace: "team", server: "https://cluster.test:6443" },
98
+ { name: "ctx-b", cluster: "cluster-b", user: "user-b", server: "https://cluster.test:6443" },
99
+ ]);
100
+ });
101
+
102
+ test("reads an address behind an auth plugin the client would refuse to run", async () => {
103
+ // `createK8sClient` throws ExecCredentialNotAllowedError here, correctly —
104
+ // it is about to run the plugin. Reading the address out of the file runs
105
+ // nothing, so refusing would report "no target" for a cluster kubectl
106
+ // reads without complaint.
107
+ const view = await readKubeconfigView({
108
+ kubeconfig: fakeKubeconfig({ exec: { command: "harvest-credentials" }, server: "https://plugin.test:6443" }),
109
+ });
110
+ expect(view.contexts).toHaveLength(1);
111
+ expect(view.contexts[0].server).toBe("https://plugin.test:6443");
112
+ });
113
+
114
+ test("a context resolving to no cluster is reported without a server, not as a failure", async () => {
115
+ const view = await readKubeconfigView({
116
+ kubeconfig: [
117
+ "apiVersion: v1",
118
+ "kind: Config",
119
+ "current-context: dangling",
120
+ "clusters: []",
121
+ "users: []",
122
+ "contexts:",
123
+ " - name: dangling",
124
+ " context:",
125
+ " cluster: gone",
126
+ " user: nobody",
127
+ "",
128
+ ].join("\n"),
129
+ });
130
+ expect(view.currentContext).toBe("dangling");
131
+ expect(view.contexts).toEqual([{ name: "dangling", cluster: "gone", user: "nobody" }]);
132
+ });
133
+
134
+ test("an unreadable or absent kubeconfig is an empty view, never a throw", async () => {
135
+ await expect(readKubeconfigView({ kubeconfigPath: join(dir, "nope.yaml") })).resolves.toEqual({ contexts: [] });
136
+ await expect(readKubeconfigView({ kubeconfig: "%%% not yaml : [" })).resolves.toEqual({ contexts: [] });
137
+ // A machine that has never seen Kubernetes: KUBECONFIG points at nothing
138
+ // that exists, and a lens over an aws-only project still renders.
139
+ process.env.KUBECONFIG = [join(dir, "a.yaml"), join(dir, "b.yaml")].join(delimiter);
140
+ await expect(readKubeconfigView()).resolves.toEqual({ contexts: [] });
141
+ });
142
+ });
143
+
144
+ describe("KUBECONFIG merge semantics match kubectl (#1630)", () => {
145
+ test("overlapping names resolve first-wins instead of failing", async () => {
146
+ // client-node's `mergeConfig` throws `Duplicate cluster: shared` here. A
147
+ // team-wide file plus a personal one both naming `dev` is the ordinary way
148
+ // people get into this, and kubectl keeps the first.
149
+ const a = write("a.yaml", configYaml({ cluster: "shared", server: "https://from-a:6443", context: "ctx-a" }));
150
+ const b = write("b.yaml", configYaml({ cluster: "shared", server: "https://from-b:6443", context: "ctx-b" }));
151
+ const kubeconfig = [a, b].join(delimiter);
152
+ process.env.KUBECONFIG = kubeconfig;
153
+
154
+ const view = await readKubeconfigView();
155
+ expect(view.contexts.map((c) => c.name)).toEqual(["ctx-a", "ctx-b"]);
156
+ // Both contexts name the cluster `shared`, and the first file's server is
157
+ // the one `shared` means.
158
+ expect(view.contexts.map((c) => c.server)).toEqual(["https://from-a:6443", "https://from-a:6443"]);
159
+
160
+ const kubectlServer = kubectlView(kubeconfig, [
161
+ "view",
162
+ "-o",
163
+ 'jsonpath={.clusters[?(@.name=="shared")].cluster.server}',
164
+ ]);
165
+ if (kubectlServer !== undefined) expect(kubectlServer).toBe("https://from-a:6443");
166
+ const kubectlContexts = kubectlView(kubeconfig, ["view", "-o", 'jsonpath={range .contexts[*]}{.name}{"\\n"}{end}']);
167
+ if (kubectlContexts !== undefined) expect(kubectlContexts.split("\n")).toEqual(["ctx-a", "ctx-b"]);
168
+ });
169
+
170
+ test("readAmbientContext answers for a duplicate-name list instead of reporting no kubeconfig", async () => {
171
+ // The load lives inside readAmbientContext's try/catch, so client-node's
172
+ // throw came out as `undefined` — chant read these users as having no
173
+ // kubeconfig at all when they have a perfectly good one.
174
+ const a = write("a.yaml", configYaml({ cluster: "shared", server: "https://from-a:6443", context: "ctx-a" }));
175
+ const b = write("b.yaml", configYaml({ cluster: "shared", server: "https://from-b:6443", context: "ctx-b" }));
176
+ process.env.KUBECONFIG = [a, b].join(delimiter);
177
+
178
+ await expect(readAmbientContext()).resolves.toBe("ctx-a");
179
+ });
180
+
181
+ test("current-context comes from the FIRST file that sets one, not the last", async () => {
182
+ const a = write("a.yaml", configYaml({ cluster: "cluster-a", server: "https://from-a:6443", context: "ctx-a" }));
183
+ const c = write("c.yaml", configYaml({ cluster: "cluster-c", server: "https://from-c:6443", context: "ctx-c" }));
184
+ const kubeconfig = [a, c].join(delimiter);
185
+ process.env.KUBECONFIG = kubeconfig;
186
+
187
+ // client-node's mergeConfig overwrites currentContext from each later
188
+ // file, so the bare loadFromDefault() answered `ctx-c` — the opposite of
189
+ // every `kubectl get` the operator ran to check.
190
+ await expect(readAmbientContext()).resolves.toBe("ctx-a");
191
+ expect((await readKubeconfigView()).currentContext).toBe("ctx-a");
192
+
193
+ const kubectlCurrent = kubectlView(kubeconfig, ["current-context"]);
194
+ if (kubectlCurrent !== undefined) expect(kubectlCurrent).toBe("ctx-a");
195
+ });
196
+
197
+ test("a file the list names but cannot be read is skipped, not fatal", async () => {
198
+ const a = write("a.yaml", configYaml({ cluster: "cluster-a", server: "https://from-a:6443", context: "ctx-a" }));
199
+ process.env.KUBECONFIG = [join(dir, "missing.yaml"), a].join(delimiter);
200
+
201
+ const view = await readKubeconfigView();
202
+ expect(view.currentContext).toBe("ctx-a");
203
+ expect(view.contexts.map((c) => c.server)).toEqual(["https://from-a:6443"]);
204
+ });
205
+
206
+ test("createK8sClient binds the cluster kubectl would bind", async () => {
207
+ const a = write("a.yaml", configYaml({ cluster: "shared", server: "https://from-a:6443", context: "ctx-a" }));
208
+ const b = write("b.yaml", configYaml({ cluster: "shared", server: "https://from-b:6443", context: "ctx-b" }));
209
+ process.env.KUBECONFIG = [a, b].join(delimiter);
210
+
211
+ // Constructing the client issues no request; the layer is here so nothing
212
+ // could reach a network if it tried.
213
+ const client = await createK8sClient({ requestLayer: fakeRequestLayer(() => ({ body: {} })) });
214
+ expect(client.provenance.server).toBe("https://from-a:6443");
215
+ expect(client.provenance.context).toBe("ctx-a");
216
+ expect(client.provenance.kubeconfigSource).toBe("default");
217
+ });
218
+
219
+ test("an explicit kubeconfig string or path is untouched by the list", async () => {
220
+ const a = write("a.yaml", configYaml({ cluster: "cluster-a", server: "https://from-a:6443", context: "ctx-a" }));
221
+ const explicit = write("explicit.yaml", configYaml({ cluster: "cluster-x", server: "https://from-x:6443", context: "ctx-x" }));
222
+ process.env.KUBECONFIG = a;
223
+
224
+ expect((await readKubeconfigView({ kubeconfigPath: explicit })).currentContext).toBe("ctx-x");
225
+ await expect(readAmbientContext({ kubeconfigPath: explicit })).resolves.toBe("ctx-x");
226
+ expect(
227
+ (await readKubeconfigView({ kubeconfig: configYaml({ cluster: "c", server: "https://s:6443", context: "ctx-lit" }) }))
228
+ .currentContext,
229
+ ).toBe("ctx-lit");
230
+ });
231
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Loading the kubeconfig the way kubectl loads it — chant #1630.
3
+ *
4
+ * `@kubernetes/client-node` parses a kubeconfig faithfully and merges a
5
+ * `KUBECONFIG` path list on two rules that are not kubectl's. Both are
6
+ * reproducible with two files and no cluster (checked against client-node
7
+ * 1.4.0), and both make chant bind a different cluster than the `kubectl get`
8
+ * the operator ran to check:
9
+ *
10
+ * - **Duplicate names throw.** `mergeConfig` funnels every entry through
11
+ * `addCluster`/`addUser`/`addContext`, each of which throws `Duplicate
12
+ * cluster: <name>` on a name it already holds. kubectl's documented rule is
13
+ * the opposite — keep the first, ignore later ones, never fail. Overlapping
14
+ * names across a list are ordinary: a team-wide file plus a personal one
15
+ * both naming `dev` is the usual way people get there. Today that throw is
16
+ * swallowed by `readAmbientContext`'s try/catch, so chant reads those users
17
+ * as having no kubeconfig at all.
18
+ * - **current-context is last-wins.** `loadFromDefault(opts,
19
+ * contextFromStartingConfig)` defaults the second argument to `false`, and
20
+ * `mergeConfig` then overwrites `currentContext` from each later file.
21
+ * kubectl takes the FIRST file that sets one.
22
+ *
23
+ * So the multi-file case is merged here instead: one `KubeConfig` per file
24
+ * (which is also what keeps each file's relative certificate paths resolved
25
+ * against its own directory), then a first-wins union by name and the first
26
+ * `current-context` that appears. Everything else — the single-file path, the
27
+ * `~/.kube/config` fallback, in-cluster credentials — stays client-node's
28
+ * `loadFromDefault`, with the one-word fix that gives it kubectl's
29
+ * current-context.
30
+ *
31
+ * A file in the list that cannot be read is skipped rather than fatal, which
32
+ * is also kubectl's rule: a stale entry in a shared `KUBECONFIG` export must
33
+ * not take the whole config down with it.
34
+ */
35
+
36
+ import { delimiter } from "node:path";
37
+ import type * as k8s from "@kubernetes/client-node";
38
+
39
+ type ClientNode = typeof import("@kubernetes/client-node");
40
+
41
+ /** Where the loaded kubeconfig came from, for {@link ClientProvenance}. */
42
+ export type KubeconfigSource = "explicit-string" | "explicit-path" | "default" | "in-cluster";
43
+
44
+ export interface LoadedKubeConfig {
45
+ kc: k8s.KubeConfig;
46
+ source: KubeconfigSource;
47
+ }
48
+
49
+ /** The `KUBECONFIG` entries, in order. Empty when the variable is unset. */
50
+ function kubeconfigPathList(): string[] {
51
+ const raw = process.env.KUBECONFIG;
52
+ if (!raw) return [];
53
+ return raw.split(delimiter).filter((f) => f.length > 0);
54
+ }
55
+
56
+ /**
57
+ * Merge a `KUBECONFIG` path list on kubectl's rules: first-wins for every
58
+ * cluster/user/context name, and the first `current-context` that any file
59
+ * sets. Unreadable files are skipped.
60
+ */
61
+ function mergeKubeconfigFiles(mod: ClientNode, files: readonly string[]): k8s.KubeConfig {
62
+ const clusters: k8s.Cluster[] = [];
63
+ const users: k8s.User[] = [];
64
+ const contexts: k8s.Context[] = [];
65
+ const seenClusters = new Set<string>();
66
+ const seenUsers = new Set<string>();
67
+ const seenContexts = new Set<string>();
68
+ let currentContext: string | undefined;
69
+
70
+ for (const file of files) {
71
+ const part = new mod.KubeConfig();
72
+ try {
73
+ part.loadFromFile(file);
74
+ } catch {
75
+ continue;
76
+ }
77
+ for (const cluster of part.getClusters()) {
78
+ if (seenClusters.has(cluster.name)) continue;
79
+ seenClusters.add(cluster.name);
80
+ clusters.push(cluster);
81
+ }
82
+ for (const user of part.getUsers()) {
83
+ if (seenUsers.has(user.name)) continue;
84
+ seenUsers.add(user.name);
85
+ users.push(user);
86
+ }
87
+ for (const context of part.getContexts()) {
88
+ if (seenContexts.has(context.name)) continue;
89
+ seenContexts.add(context.name);
90
+ contexts.push(context);
91
+ }
92
+ if (currentContext === undefined) {
93
+ const current = part.getCurrentContext();
94
+ if (current) currentContext = current;
95
+ }
96
+ }
97
+
98
+ const kc = new mod.KubeConfig();
99
+ kc.loadFromOptions({ clusters, users, contexts, currentContext });
100
+ return kc;
101
+ }
102
+
103
+ /**
104
+ * The kubeconfig this invocation should read, by the same precedence every
105
+ * entry point in this package uses: an explicit literal, then an explicit
106
+ * path, then the ambient default (`KUBECONFIG`, `~/.kube/config`, in-cluster).
107
+ *
108
+ * Throws whatever client-node throws for an explicit string or path — those
109
+ * are the caller naming a file, and a caller who names an unreadable one wants
110
+ * to hear about it. The ambient path never throws for a duplicate name.
111
+ */
112
+ export function loadKubeConfig(
113
+ mod: ClientNode,
114
+ options: { kubeconfig?: string; kubeconfigPath?: string } = {},
115
+ ): LoadedKubeConfig {
116
+ if (options.kubeconfig !== undefined) {
117
+ const kc = new mod.KubeConfig();
118
+ kc.loadFromString(options.kubeconfig);
119
+ return { kc, source: "explicit-string" };
120
+ }
121
+ if (options.kubeconfigPath !== undefined) {
122
+ const kc = new mod.KubeConfig();
123
+ kc.loadFromFile(options.kubeconfigPath);
124
+ return { kc, source: "explicit-path" };
125
+ }
126
+
127
+ const files = kubeconfigPathList();
128
+ if (files.length > 1) return { kc: mergeKubeconfigFiles(mod, files), source: "default" };
129
+
130
+ const kc = new mod.KubeConfig();
131
+ // `true` is the whole current-context fix for the single-file and
132
+ // `~/.kube/config` paths: it tells client-node to keep the starting config's
133
+ // context rather than letting a later merge overwrite it.
134
+ kc.loadFromDefault(undefined, true);
135
+ return { kc, source: kc.getCurrentContext() === "inCluster" ? "in-cluster" : "default" };
136
+ }
package/src/types.ts CHANGED
@@ -95,6 +95,27 @@ export interface ClientProvenance {
95
95
  kubeconfigSource: "explicit-string" | "explicit-path" | "default" | "in-cluster";
96
96
  }
97
97
 
98
+ /**
99
+ * One context of the merged kubeconfig, joined to the apiserver its cluster
100
+ * resolves to (chant #1630). Plain data read out of a file: no credential is
101
+ * touched, no exec plugin runs, no request is issued.
102
+ */
103
+ export interface KubeconfigContextInfo {
104
+ name: string;
105
+ /** Cluster name the context names. Empty when the context declares none. */
106
+ cluster: string;
107
+ user?: string;
108
+ namespace?: string;
109
+ /** The apiserver URL the named cluster resolves to, when the config has one. */
110
+ server?: string;
111
+ }
112
+
113
+ /** What {@link import("./client").readKubeconfigView} returns. */
114
+ export interface KubeconfigView {
115
+ contexts: KubeconfigContextInfo[];
116
+ currentContext?: string;
117
+ }
118
+
98
119
  /** Options for {@link import("./client").createK8sClient}. */
99
120
  export interface K8sClientOptions {
100
121
  /**