Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
faac313
chore: regenerate
mstruebing Feb 7, 2026
25b153d
Initial plan
Copilot Mar 12, 2026
c96c4be
fix: migrate from node-fetch agent to undici dispatcher for API client
Copilot Mar 12, 2026
e0fd44b
fix: restore test verifications and migrate remaining files to undici
Copilot Mar 16, 2026
2e86fcf
fix: restore test verifications and migrate all tests to undici MockA…
Copilot Mar 16, 2026
0211177
Merge pull request #2794 from kubernetes-client/copilot/sub-pr-2770
brendandburns Mar 17, 2026
762cce9
Initial plan
Copilot Mar 17, 2026
9726838
fix: downgrade undici to v6.x to fix File is not defined on Node.js 18
Copilot Mar 17, 2026
53db566
Merge pull request #2797 from kubernetes-client/copilot/sub-pr-2770-a…
brendandburns Mar 17, 2026
77fe123
Initial plan
Copilot Mar 17, 2026
4ac029a
fix: move 3 incorrectly nested tests to proper scope in object_test.ts
Copilot Mar 17, 2026
596aedf
Merge pull request #2798 from kubernetes-client/copilot/sub-pr-2770-a…
brendandburns Mar 17, 2026
c9bcff5
Initial plan
Copilot Mar 17, 2026
f75ff67
Initial plan
Copilot Mar 18, 2026
ec0a01f
fix: restore coverage for proxy agent paths and add missing contextG …
Copilot Mar 18, 2026
b15d9bd
revert: restore settings file to original state
Copilot Mar 18, 2026
61b081f
Merge pull request #2799 from kubernetes-client/copilot/sub-pr-2770-y…
brendandburns Mar 18, 2026
75b55aa
Initial plan
Copilot Mar 18, 2026
f488395
refactor: use t.after() hooks instead of try/finally in tests
Copilot Mar 18, 2026
fb174cc
support socks proxy
mstruebing Mar 19, 2026
bd668f6
refactor to createDispatcherOptions
davidgamero Mar 19, 2026
c0c3221
fix: remove unused DispatcherOptions import from config_test
davidgamero Mar 19, 2026
ff76b1a
refactor: use tls.ConnectionOptions and rename connectOptions to tlsO…
davidgamero Mar 19, 2026
7d2213d
Merge pull request #2800 from kubernetes-client/copilot/sub-pr-2770-o…
brendandburns Mar 19, 2026
670f178
refactor: remove redundant if blocks, rely on strictEqual assertion n…
davidgamero Mar 19, 2026
09c8201
Merge pull request #2801 from davidgamero/davidgamero/improve-dispatc…
k8s-ci-robot Mar 19, 2026
d4dc905
Initial plan
Copilot Mar 19, 2026
a68e173
fix: downgrade undici from v7 to v6, restore SOCKS proxy error for un…
Copilot Mar 19, 2026
2152769
feat: add SOCKS proxy support to undici dispatcher path using SocksCl…
Copilot Mar 20, 2026
ab79f0e
Merge pull request #2802 from kubernetes-client/copilot/sub-pr-2770-p…
brendandburns Mar 23, 2026
84da37f
Merge branch 'main' into max-regen-gen
brendandburns Mar 23, 2026
cb73a16
style: format src/config.ts with prettier
davidgamero Mar 24, 2026
605d3f7
Merge pull request #2806 from davidgamero/fix/prettier-config
k8s-ci-robot Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@
"node-fetch": "^2.7.0",
"openid-client": "^6.1.3",
"rfc4648": "^1.3.0",
"socks": "^2.8.4",
"socks-proxy-agent": "^9.0.0",
"stream-buffers": "^3.0.2",
"tar-fs": "^3.0.9",
"undici": "^6.24.1",
"ws": "^8.18.2"
},
"devDependencies": {
Expand All @@ -85,7 +87,7 @@
"prettier": "^3.0.0",
"pretty-quick": "^4.0.0",
"ts-mockito": "^2.3.1",
"tsx": "^4.19.1",
"tsx": "^4.21.0",
"typedoc": "^0.28.0",
"typescript": "~5.9.2",
"typescript-eslint": "^8.26.0"
Expand Down
2 changes: 1 addition & 1 deletion settings
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ export CLIENT_VERSION="0.8-SNAPSHOT"
# Name of the release package
export PACKAGE_NAME="@kubernetes/node-client"

export OPENAPI_GENERATOR_COMMIT=6e0fe098f1d9631c696135c7a3c46e4b0dc9ab3f
export OPENAPI_GENERATOR_COMMIT=9fa18d0c8102322039676a9d11107a7cd00bf6ae
14 changes: 10 additions & 4 deletions src/azure_auth_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { User, Cluster } from './config_types.js';
import { AzureAuth } from './azure_auth.js';
import { KubeConfig } from './config.js';
import { HttpMethod, RequestContext } from './index.js';
import { Agent as UndiciAgent } from 'undici';

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -105,8 +106,13 @@ describe('AzureAuth', () => {
const requestContext = new RequestContext(testUrl1, HttpMethod.GET);

await config.applySecurityAuthentication(requestContext);
// @ts-expect-error
strictEqual(requestContext.getAgent().options.rejectUnauthorized, false);
const dispatcher = requestContext.getDispatcher() as UndiciAgent;
strictEqual(dispatcher instanceof UndiciAgent, true);
const dispatcherOpts = config.createDispatcherOptions({ skipTLSVerify: true } as Cluster, {
rejectUnauthorized: false,
});
strictEqual(dispatcherOpts.type, 'agent');
strictEqual(dispatcherOpts.connect.rejectUnauthorized, false);
});

it('should not set rejectUnauthorized if skipTLSVerify is not set', async () => {
Expand All @@ -128,8 +134,8 @@ describe('AzureAuth', () => {
const requestContext = new RequestContext(testUrl1, HttpMethod.GET);

await config.applySecurityAuthentication(requestContext);
// @ts-expect-error
strictEqual(requestContext.getAgent().options.rejectUnauthorized, undefined);
// When skipTLSVerify is not set, no custom dispatcher is needed - undici validates certs by default
strictEqual(requestContext.getDispatcher(), undefined);
Comment on lines +137 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem all that helpful for validating anything.

});

it('should throw with expired token and no cmd', async () => {
Expand Down
123 changes: 122 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import fs from 'node:fs';
import https from 'node:https';
import http from 'node:http';
import tls from 'node:tls';
import yaml from 'js-yaml';
import net from 'node:net';
import path from 'node:path';

import { Headers, RequestInit } from 'node-fetch';
import {
Agent as UndiciAgent,
ProxyAgent as UndiciProxyAgent,
buildConnector,
type Dispatcher,
} from 'undici';
import { RequestContext } from './api.js';
import { Authenticator } from './auth.js';
import { AzureAuth } from './azure_auth.js';
Expand Down Expand Up @@ -35,10 +42,21 @@ import { OpenIDConnectAuth } from './oidc_auth.js';
import WebSocket from 'isomorphic-ws';
import child_process from 'node:child_process';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { SocksClient } from 'socks';
import { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, HttpsProxyAgentOptions } from 'hpagent';
import packagejson from '../package.json' with { type: 'json' };
import { setHeaderMiddleware } from './middleware.js';

// Uses tls.ConnectionOptions for the TLS connect fields we populate (ca, cert, key, etc.).
// For the full set of constructor options available when creating dispatchers, see:
// - Agent: Agent.Options (extends Pool.Options -> Client.Options)
// - ProxyAgent: ProxyAgent.Options (extends Agent.Options, adds uri, requestTls, proxyTls, etc.)
export type DispatcherOptions =
| { type: 'proxy'; uri: string; requestTls: tls.ConnectionOptions }
| { type: 'socks'; uri: string; requestTls: tls.ConnectionOptions }
| { type: 'agent'; connect: tls.ConnectionOptions }
| { type: 'none' };

const SERVICEACCOUNT_ROOT: string = '/var/run/secrets/kubernetes.io/serviceaccount';
const SERVICEACCOUNT_CA_PATH: string = SERVICEACCOUNT_ROOT + '/ca.crt';
const SERVICEACCOUNT_TOKEN_PATH: string = SERVICEACCOUNT_ROOT + '/token';
Expand All @@ -61,6 +79,51 @@ function fileExists(filepath: string): boolean {
}
}

/**
* Creates an undici-compatible connector function that tunnels connections
* through a SOCKS proxy (v4/v4a/v5/v5h).
*/
function createSocksConnector(proxyUrl: string, tlsOptions: tls.ConnectionOptions): buildConnector.connector {
const parsedProxy = new URL(proxyUrl);
const proxyHost = parsedProxy.hostname;
const proxyPort = parseInt(parsedProxy.port, 10) || 1080;
let socksType: 4 | 5 = 5;
const proto = parsedProxy.protocol.replace(':', '');
if (proto === 'socks4' || proto === 'socks4a') {
socksType = 4;
}

return (options, callback) => {
const { hostname, port, protocol, servername } = options;
SocksClient.createConnection(
{
proxy: { host: proxyHost, port: proxyPort, type: socksType },
command: 'connect',
destination: { host: hostname, port: parseInt(port, 10) },
},
(err, info) => {
if (err) {
callback(err, null);
return;
}
const socket = info!.socket;
if (protocol === 'https:') {
callback(
null,
tls.connect({
...tlsOptions,
socket,
servername: servername || hostname,
}),
);
} else {
callback(null, socket);
}
},
);
};
}

// TODO: the empty interface breaks the linter, but this type
// will be needed later to get the object and cache features working again
export interface ApiType {}
Expand Down Expand Up @@ -275,7 +338,10 @@ export class KubeConfig implements SecurityAuthentication {
agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized;
}

context.setAgent(this.createAgent(cluster, agentOptions));
const dispatcher = this.createDispatcher(cluster, agentOptions);
if (dispatcher !== undefined) {
context.setDispatcher(dispatcher);
}
}

/**
Expand Down Expand Up @@ -571,6 +637,61 @@ export class KubeConfig implements SecurityAuthentication {
return agent;
}

/**
* Build the dispatcher configuration (options + type) without constructing
* the actual Dispatcher instance. Exposed as a separate method so that
* tests can validate the option-mapping logic directly instead of reaching
* into undici's Symbol-keyed private state.
*/
public createDispatcherOptions(
cluster: Cluster | null,
agentOptions: https.AgentOptions,
): DispatcherOptions {
const tlsOptions: tls.ConnectionOptions = {};
if (agentOptions.ca !== undefined) tlsOptions.ca = agentOptions.ca;
if (agentOptions.cert !== undefined) tlsOptions.cert = agentOptions.cert;
if (agentOptions.key !== undefined) tlsOptions.key = agentOptions.key;
if (agentOptions.pfx !== undefined) tlsOptions.pfx = agentOptions.pfx;
if (agentOptions.passphrase !== undefined) tlsOptions.passphrase = agentOptions.passphrase;
if (agentOptions.rejectUnauthorized !== undefined)
tlsOptions.rejectUnauthorized = agentOptions.rejectUnauthorized;
if ((agentOptions as any).servername !== undefined)
tlsOptions.servername = (agentOptions as any).servername;

if (cluster && cluster.proxyUrl) {
if (cluster.proxyUrl.startsWith('socks')) {
return { type: 'socks', uri: cluster.proxyUrl, requestTls: tlsOptions };
}
if (!cluster.server.startsWith('https') && !cluster.server.startsWith('http')) {
throw new Error('Unsupported proxy type');
}
return { type: 'proxy', uri: cluster.proxyUrl, requestTls: tlsOptions };
} else if (cluster?.server?.startsWith('http:') && !cluster.skipTLSVerify) {
throw new Error('HTTP protocol is not allowed when skipTLSVerify is not set or false');
}
if (Object.keys(tlsOptions).length === 0) {
return { type: 'none' };
}
return { type: 'agent', connect: tlsOptions };
}

private createDispatcher(
cluster: Cluster | null,
agentOptions: https.AgentOptions,
): Dispatcher | undefined {
const opts = this.createDispatcherOptions(cluster, agentOptions);
switch (opts.type) {
case 'proxy':
return new UndiciProxyAgent({ uri: opts.uri, requestTls: opts.requestTls });
case 'socks':
return new UndiciAgent({ connect: createSocksConnector(opts.uri, opts.requestTls) });
case 'agent':
return new UndiciAgent({ connect: opts.connect });
case 'none':
return undefined;
}
}

private applyHTTPSOptions(opts: https.RequestOptions | WebSocket.ClientOptions): void {
const cluster = this.getCurrentCluster();
const user = this.getCurrentUser();
Expand Down
Loading
Loading