Skip to content

Commit a96d1c7

Browse files
committed
fix client to be able to be called from within cluster, docs for auth
1 parent cc6f6fd commit a96d1c7

4 files changed

Lines changed: 466 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
KubernetesJS is a monorepo providing a fully-typed, zero-dependency TypeScript client for the entire Kubernetes API, plus React hooks, CLIs, and operational tooling. Code is generated from the Kubernetes OpenAPI spec using `schema-sdk`.
88

9+
## DeepWiki
10+
11+
- **This repo**: https://deepwiki.com/constructive-io/kubernetesjs
12+
- **Kubernetes client-go** (reference Go client): https://deepwiki.com/kubernetes/client-go
13+
914
## Build & Development Commands
1015

1116
```bash

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ Start the Kubernetes API proxy to allow local access:
202202
kubectl proxy --port=8001 --accept-hosts='^.*$' --address='0.0.0.0'
203203
```
204204

205+
## Documentation
206+
207+
Explore the full architecture and internals via [DeepWiki](https://deepwiki.com/constructive-io/kubernetesjs).
208+
205209
## Related
206210

207211
Checkout these related projects:

docs/auth.md

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
# Authentication & Client Configuration
2+
3+
This document covers how `kubernetesjs` handles authentication, endpoint detection, and client configuration. It also compares the approach with Go's `client-go` and outlines future work.
4+
5+
## Quick Reference
6+
7+
```typescript
8+
import { KubernetesClient } from 'kubernetesjs';
9+
10+
// Auto-detect: in-cluster if available, else kubectl proxy at localhost:8001
11+
const client = new KubernetesClient(KubernetesClient.getDefaultConfig());
12+
13+
// Explicit in-cluster (throws if not in a pod)
14+
const client = new KubernetesClient(KubernetesClient.getInClusterConfig());
15+
16+
// Explicit token
17+
const client = new KubernetesClient({
18+
restEndpoint: 'https://my-cluster:6443',
19+
token: myBearerToken,
20+
});
21+
22+
// kubectl proxy (backward-compatible, no auth needed)
23+
const client = new KubernetesClient({ restEndpoint: 'http://127.0.0.1:8001' });
24+
```
25+
26+
## Constructor Options
27+
28+
```typescript
29+
interface APIClientOptions {
30+
restEndpoint: string; // Required: Kubernetes API URL
31+
token?: string; // Bearer token (SA, OIDC, etc.)
32+
headers?: Record<string, string>; // Default headers for every request
33+
timeout?: number; // Default timeout in ms (default: 10000)
34+
}
35+
```
36+
37+
All fields except `restEndpoint` are optional and backward-compatible.
38+
39+
### Header Merge Order
40+
41+
When a request is made, headers are merged with this priority (highest wins):
42+
43+
1. **Per-request headers** (passed via `opts.headers` on individual API calls)
44+
2. **Auth header** (auto-injected `Authorization: Bearer <token>` if `token` is set)
45+
3. **Default headers** (from `options.headers` in constructor)
46+
47+
This means per-request headers can override the default token if needed.
48+
49+
## Static Helpers
50+
51+
These are defined on `APIClient` and inherited by `KubernetesClient`, so you can call them as `KubernetesClient.methodName()`.
52+
53+
### `detectEndpoint(fallback?)`
54+
55+
Detects the Kubernetes API endpoint from environment variables.
56+
57+
**Priority:**
58+
1. `KUBERNETES_API_URL` — explicit override (custom env var for flexibility)
59+
2. `KUBERNETES_SERVICE_HOST` + `KUBERNETES_SERVICE_PORT` — standard in-cluster env vars set by Kubernetes
60+
3. `fallback` — defaults to `http://127.0.0.1:8001` (kubectl proxy)
61+
62+
```typescript
63+
const endpoint = KubernetesClient.detectEndpoint();
64+
// In-cluster: "https://10.96.0.1:443"
65+
// Local dev: "http://127.0.0.1:8001"
66+
```
67+
68+
### `readServiceAccountToken(path?)`
69+
70+
Reads the mounted service account bearer token from the filesystem. Returns `undefined` if the file doesn't exist or isn't readable.
71+
72+
**Default path:** `/var/run/secrets/kubernetes.io/serviceaccount/token`
73+
74+
```typescript
75+
const token = KubernetesClient.readServiceAccountToken();
76+
if (token) {
77+
console.log('Running in-cluster with SA token');
78+
}
79+
```
80+
81+
> **Node.js only** — uses `require('node:fs')`. Returns `undefined` in browser environments.
82+
83+
### `getServiceAccountCAPath()`
84+
85+
Returns the path to the service account CA certificate if it exists at the standard mount point (`/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`), or `undefined` if not found.
86+
87+
Use this to set `NODE_EXTRA_CA_CERTS` before starting your application so that `fetch()` trusts the cluster's API server certificate:
88+
89+
```bash
90+
NODE_EXTRA_CA_CERTS=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt node app.js
91+
```
92+
93+
### `getInClusterConfig()`
94+
95+
Builds a complete `APIClientOptions` for in-cluster usage. Reads the SA token and detects the endpoint from environment variables.
96+
97+
**Equivalent to client-go's `rest.InClusterConfig()`.**
98+
99+
Throws if:
100+
- The SA token file is not found
101+
- `KUBERNETES_SERVICE_HOST` is not set
102+
103+
```typescript
104+
try {
105+
const config = KubernetesClient.getInClusterConfig();
106+
const client = new KubernetesClient(config);
107+
} catch (err) {
108+
console.error('Not running in-cluster:', err.message);
109+
}
110+
```
111+
112+
### `getDefaultConfig(fallbackEndpoint?)`
113+
114+
Tries in-cluster config first, falls back to kubectl proxy.
115+
116+
**Equivalent to client-go's `BuildConfigFromFlags("", "")`.**
117+
118+
```typescript
119+
// Always works — in-cluster or local dev
120+
const client = new KubernetesClient(KubernetesClient.getDefaultConfig());
121+
```
122+
123+
## Environment Variables
124+
125+
| Variable | Used By | Description |
126+
|---|---|---|
127+
| `KUBERNETES_API_URL` | `detectEndpoint()` | Explicit API server URL override |
128+
| `KUBERNETES_SERVICE_HOST` | `detectEndpoint()` | In-cluster API server host (set by K8s) |
129+
| `KUBERNETES_SERVICE_PORT` | `detectEndpoint()` | In-cluster API server port (set by K8s, default: 443) |
130+
| `NODE_EXTRA_CA_CERTS` | Node.js TLS | Path to CA cert for HTTPS verification |
131+
132+
## In-Cluster Usage
133+
134+
When running inside a Kubernetes pod, the kubelet automatically mounts:
135+
136+
| File | Path | Purpose |
137+
|---|---|---|
138+
| SA Token | `/var/run/secrets/kubernetes.io/serviceaccount/token` | Bearer token for API auth |
139+
| CA Cert | `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` | Cluster CA for TLS verification |
140+
| Namespace | `/var/run/secrets/kubernetes.io/serviceaccount/namespace` | Pod's namespace |
141+
142+
And sets environment variables:
143+
- `KUBERNETES_SERVICE_HOST` — API server IP
144+
- `KUBERNETES_SERVICE_PORT` — API server port (usually 443)
145+
146+
### Minimal In-Cluster Example
147+
148+
```dockerfile
149+
FROM node:20-alpine
150+
ENV NODE_EXTRA_CA_CERTS=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
151+
COPY . /app
152+
CMD ["node", "/app/index.js"]
153+
```
154+
155+
```typescript
156+
import { KubernetesClient } from 'kubernetesjs';
157+
158+
const client = new KubernetesClient(KubernetesClient.getInClusterConfig());
159+
const pods = await client.listCoreV1NamespacedPod({ path: { namespace: 'default' } });
160+
```
161+
162+
### RBAC Requirements
163+
164+
The pod's service account needs appropriate RBAC permissions:
165+
166+
```yaml
167+
apiVersion: rbac.authorization.k8s.io/v1
168+
kind: ClusterRole
169+
metadata:
170+
name: my-app-role
171+
rules:
172+
- apiGroups: [""]
173+
resources: ["pods", "services", "namespaces"]
174+
verbs: ["get", "list", "watch", "create", "update", "delete"]
175+
---
176+
apiVersion: rbac.authorization.k8s.io/v1
177+
kind: ClusterRoleBinding
178+
metadata:
179+
name: my-app-binding
180+
subjects:
181+
- kind: ServiceAccount
182+
name: default
183+
namespace: my-namespace
184+
roleRef:
185+
kind: ClusterRole
186+
name: my-app-role
187+
apiGroup: rbac.authorization.k8s.io
188+
```
189+
190+
## Comparison with client-go
191+
192+
| Feature | client-go | kubernetesjs | Status |
193+
|---|---|---|---|
194+
| In-cluster config (SA token + env detection) | `rest.InClusterConfig()` | `getInClusterConfig()` | Supported |
195+
| Auto-detect (in-cluster → fallback) | `BuildConfigFromFlags("","")` | `getDefaultConfig()` | Supported |
196+
| Bearer token auth | `BearerToken` field | `token` option | Supported |
197+
| Custom headers | Transport wrappers | `headers` option | Supported |
198+
| SA token file path | `BearerTokenFile` | `readServiceAccountToken(path)` | Supported |
199+
| CA certificate | `TLSClientConfig.CAFile` | `NODE_EXTRA_CA_CERTS` env var | Partial |
200+
| Kubeconfig parsing (`~/.kube/config`) | `clientcmd` package | — | Planned |
201+
| Multiple kubeconfig merge (`KUBECONFIG`) | `ClientConfigLoadingRules` | — | Planned |
202+
| Context switching | `--context` flag | — | Planned |
203+
| Client certificate auth (mTLS) | `TLSClientConfig.CertFile/KeyFile` | — | Planned |
204+
| Exec-based credential plugins | `ExecProvider` | — | Planned |
205+
| OIDC / auth provider plugins | `AuthProvider` | — | Planned |
206+
| Token file watching (auto-refresh) | `BearerTokenFile` + transport | — | Planned |
207+
| Basic auth (username/password) | `Username`/`Password` | — | Not planned |
208+
| Impersonation headers | `Impersonate` config | — | Planned |
209+
| HTTP proxy support | `ProxyURL` / env vars | — | Planned |
210+
| Connection backoff | `KUBE_CLIENT_BACKOFF_*` env vars | — | Planned |
211+
212+
### How client-go Handles Auth Internally
213+
214+
client-go uses a layered transport system with `http.RoundTripper` wrappers:
215+
216+
1. **Config loading**: `rest.Config` is built from kubeconfig or in-cluster detection
217+
2. **Transport wrapping**: `HTTPWrappersForConfig` wraps a base transport with:
218+
- `BearerAuthWithRefreshRoundTripper` — reads token, re-reads `BearerTokenFile` on each request
219+
- `BasicAuthRoundTripper` — adds `Authorization: Basic` header
220+
- `ImpersonatingRoundTripper` — adds `Impersonate-*` headers
221+
- `UserAgentRoundTripper` — adds `User-Agent` header
222+
3. **TLS**: Custom `tls.Config` with cluster CA, client certs, SNI, minimum TLS 1.2
223+
224+
The `InClusterConfig()` flow specifically:
225+
1. Check `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` — if missing, return `ErrNotInCluster`
226+
2. Read token from `/var/run/secrets/kubernetes.io/serviceaccount/token`
227+
3. Read CA cert from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` (non-fatal if missing)
228+
4. Return `rest.Config` with `Host`, `BearerToken`, `BearerTokenFile`, and `TLSClientConfig.CAFile`
229+
230+
## Future Work
231+
232+
### Kubeconfig Support
233+
234+
Parsing `~/.kube/config` (or `$KUBECONFIG`) to extract cluster endpoints, contexts, and user credentials. This would enable:
235+
236+
```typescript
237+
// Future API
238+
const client = new KubernetesClient(
239+
KubernetesClient.fromKubeConfig({ context: 'my-cluster' })
240+
);
241+
```
242+
243+
**Challenge**: Requires a YAML parser. Options include adding `js-yaml` as an optional peer dependency, or implementing minimal YAML parsing for the kubeconfig subset.
244+
245+
### TLS / CA Certificate Handling
246+
247+
Native support for custom CA certificates without relying on `NODE_EXTRA_CA_CERTS`. This would require creating a custom `fetch` agent with TLS options:
248+
249+
```typescript
250+
// Future API
251+
const client = new KubernetesClient({
252+
restEndpoint: 'https://my-cluster:6443',
253+
token: myToken,
254+
tls: {
255+
caFile: '/path/to/ca.crt',
256+
// or caData: Buffer.from('...')
257+
insecureSkipVerify: false, // for dev only
258+
}
259+
});
260+
```
261+
262+
### Client Certificate Auth (mTLS)
263+
264+
```typescript
265+
// Future API
266+
const client = new KubernetesClient({
267+
restEndpoint: 'https://my-cluster:6443',
268+
tls: {
269+
certFile: '/path/to/client.crt',
270+
keyFile: '/path/to/client.key',
271+
caFile: '/path/to/ca.crt',
272+
}
273+
});
274+
```
275+
276+
### Token File Watching
277+
278+
Automatic re-reading of the SA token file on each request (or periodically), supporting Kubernetes bound service account token rotation:
279+
280+
```typescript
281+
// Future API
282+
const client = new KubernetesClient({
283+
restEndpoint: 'https://...',
284+
tokenFile: '/var/run/secrets/kubernetes.io/serviceaccount/token',
285+
// Token is re-read from file before each request
286+
});
287+
```
288+
289+
### Exec-Based Credential Plugins
290+
291+
Support for external credential commands (used by GKE, EKS, AKS):
292+
293+
```typescript
294+
// Future API
295+
const client = new KubernetesClient({
296+
restEndpoint: 'https://...',
297+
exec: {
298+
command: 'gke-gcloud-auth-plugin',
299+
apiVersion: 'client.authentication.k8s.io/v1beta1',
300+
}
301+
});
302+
```
303+
304+
### OIDC / Auth Provider Plugins
305+
306+
OpenID Connect token authentication for identity-based access.
307+
308+
### Impersonation
309+
310+
```typescript
311+
// Future API
312+
const client = new KubernetesClient({
313+
restEndpoint: 'https://...',
314+
token: adminToken,
315+
impersonate: {
316+
user: 'jane@example.com',
317+
groups: ['developers'],
318+
}
319+
});
320+
```

0 commit comments

Comments
 (0)