Skip to content

Commit f97942f

Browse files
authored
Merge pull request #2770 from kubernetes-client/max-regen-gen
chore: regenerate
2 parents 07ff83d + 605d3f7 commit f97942f

19 files changed

Lines changed: 1464 additions & 718 deletions

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@
6868
"node-fetch": "^2.7.0",
6969
"openid-client": "^6.1.3",
7070
"rfc4648": "^1.3.0",
71+
"socks": "^2.8.4",
7172
"socks-proxy-agent": "^9.0.0",
7273
"stream-buffers": "^3.0.2",
7374
"tar-fs": "^3.0.9",
75+
"undici": "^6.24.1",
7476
"ws": "^8.18.2"
7577
},
7678
"devDependencies": {
@@ -85,7 +87,7 @@
8587
"prettier": "^3.0.0",
8688
"pretty-quick": "^4.0.0",
8789
"ts-mockito": "^2.3.1",
88-
"tsx": "^4.19.1",
90+
"tsx": "^4.21.0",
8991
"typedoc": "^0.28.0",
9092
"typescript": "~5.9.2",
9193
"typescript-eslint": "^8.26.0"

settings

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ export CLIENT_VERSION="0.8-SNAPSHOT"
3030
# Name of the release package
3131
export PACKAGE_NAME="@kubernetes/node-client"
3232

33-
export OPENAPI_GENERATOR_COMMIT=6e0fe098f1d9631c696135c7a3c46e4b0dc9ab3f
33+
export OPENAPI_GENERATOR_COMMIT=9fa18d0c8102322039676a9d11107a7cd00bf6ae

src/azure_auth_test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { User, Cluster } from './config_types.js';
77
import { AzureAuth } from './azure_auth.js';
88
import { KubeConfig } from './config.js';
99
import { HttpMethod, RequestContext } from './index.js';
10+
import { Agent as UndiciAgent } from 'undici';
1011

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

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

107108
await config.applySecurityAuthentication(requestContext);
108-
// @ts-expect-error
109-
strictEqual(requestContext.getAgent().options.rejectUnauthorized, false);
109+
const dispatcher = requestContext.getDispatcher() as UndiciAgent;
110+
strictEqual(dispatcher instanceof UndiciAgent, true);
111+
const dispatcherOpts = config.createDispatcherOptions({ skipTLSVerify: true } as Cluster, {
112+
rejectUnauthorized: false,
113+
});
114+
strictEqual(dispatcherOpts.type, 'agent');
115+
strictEqual(dispatcherOpts.connect.rejectUnauthorized, false);
110116
});
111117

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

130136
await config.applySecurityAuthentication(requestContext);
131-
// @ts-expect-error
132-
strictEqual(requestContext.getAgent().options.rejectUnauthorized, undefined);
137+
// When skipTLSVerify is not set, no custom dispatcher is needed - undici validates certs by default
138+
strictEqual(requestContext.getDispatcher(), undefined);
133139
});
134140

135141
it('should throw with expired token and no cmd', async () => {

src/config.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import fs from 'node:fs';
22
import https from 'node:https';
33
import http from 'node:http';
4+
import tls from 'node:tls';
45
import yaml from 'js-yaml';
56
import net from 'node:net';
67
import path from 'node:path';
78

89
import { Headers, RequestInit } from 'node-fetch';
10+
import {
11+
Agent as UndiciAgent,
12+
ProxyAgent as UndiciProxyAgent,
13+
buildConnector,
14+
type Dispatcher,
15+
} from 'undici';
916
import { RequestContext } from './api.js';
1017
import { Authenticator } from './auth.js';
1118
import { AzureAuth } from './azure_auth.js';
@@ -35,10 +42,21 @@ import { OpenIDConnectAuth } from './oidc_auth.js';
3542
import WebSocket from 'isomorphic-ws';
3643
import child_process from 'node:child_process';
3744
import { SocksProxyAgent } from 'socks-proxy-agent';
45+
import { SocksClient } from 'socks';
3846
import { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, HttpsProxyAgentOptions } from 'hpagent';
3947
import packagejson from '../package.json' with { type: 'json' };
4048
import { setHeaderMiddleware } from './middleware.js';
4149

50+
// Uses tls.ConnectionOptions for the TLS connect fields we populate (ca, cert, key, etc.).
51+
// For the full set of constructor options available when creating dispatchers, see:
52+
// - Agent: Agent.Options (extends Pool.Options -> Client.Options)
53+
// - ProxyAgent: ProxyAgent.Options (extends Agent.Options, adds uri, requestTls, proxyTls, etc.)
54+
export type DispatcherOptions =
55+
| { type: 'proxy'; uri: string; requestTls: tls.ConnectionOptions }
56+
| { type: 'socks'; uri: string; requestTls: tls.ConnectionOptions }
57+
| { type: 'agent'; connect: tls.ConnectionOptions }
58+
| { type: 'none' };
59+
4260
const SERVICEACCOUNT_ROOT: string = '/var/run/secrets/kubernetes.io/serviceaccount';
4361
const SERVICEACCOUNT_CA_PATH: string = SERVICEACCOUNT_ROOT + '/ca.crt';
4462
const SERVICEACCOUNT_TOKEN_PATH: string = SERVICEACCOUNT_ROOT + '/token';
@@ -61,6 +79,51 @@ function fileExists(filepath: string): boolean {
6179
}
6280
}
6381

82+
/**
83+
* Creates an undici-compatible connector function that tunnels connections
84+
* through a SOCKS proxy (v4/v4a/v5/v5h).
85+
*/
86+
function createSocksConnector(proxyUrl: string, tlsOptions: tls.ConnectionOptions): buildConnector.connector {
87+
const parsedProxy = new URL(proxyUrl);
88+
const proxyHost = parsedProxy.hostname;
89+
const proxyPort = parseInt(parsedProxy.port, 10) || 1080;
90+
let socksType: 4 | 5 = 5;
91+
const proto = parsedProxy.protocol.replace(':', '');
92+
if (proto === 'socks4' || proto === 'socks4a') {
93+
socksType = 4;
94+
}
95+
96+
return (options, callback) => {
97+
const { hostname, port, protocol, servername } = options;
98+
SocksClient.createConnection(
99+
{
100+
proxy: { host: proxyHost, port: proxyPort, type: socksType },
101+
command: 'connect',
102+
destination: { host: hostname, port: parseInt(port, 10) },
103+
},
104+
(err, info) => {
105+
if (err) {
106+
callback(err, null);
107+
return;
108+
}
109+
const socket = info!.socket;
110+
if (protocol === 'https:') {
111+
callback(
112+
null,
113+
tls.connect({
114+
...tlsOptions,
115+
socket,
116+
servername: servername || hostname,
117+
}),
118+
);
119+
} else {
120+
callback(null, socket);
121+
}
122+
},
123+
);
124+
};
125+
}
126+
64127
// TODO: the empty interface breaks the linter, but this type
65128
// will be needed later to get the object and cache features working again
66129
export interface ApiType {}
@@ -275,7 +338,10 @@ export class KubeConfig implements SecurityAuthentication {
275338
agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized;
276339
}
277340

278-
context.setAgent(this.createAgent(cluster, agentOptions));
341+
const dispatcher = this.createDispatcher(cluster, agentOptions);
342+
if (dispatcher !== undefined) {
343+
context.setDispatcher(dispatcher);
344+
}
279345
}
280346

281347
/**
@@ -571,6 +637,61 @@ export class KubeConfig implements SecurityAuthentication {
571637
return agent;
572638
}
573639

640+
/**
641+
* Build the dispatcher configuration (options + type) without constructing
642+
* the actual Dispatcher instance. Exposed as a separate method so that
643+
* tests can validate the option-mapping logic directly instead of reaching
644+
* into undici's Symbol-keyed private state.
645+
*/
646+
public createDispatcherOptions(
647+
cluster: Cluster | null,
648+
agentOptions: https.AgentOptions,
649+
): DispatcherOptions {
650+
const tlsOptions: tls.ConnectionOptions = {};
651+
if (agentOptions.ca !== undefined) tlsOptions.ca = agentOptions.ca;
652+
if (agentOptions.cert !== undefined) tlsOptions.cert = agentOptions.cert;
653+
if (agentOptions.key !== undefined) tlsOptions.key = agentOptions.key;
654+
if (agentOptions.pfx !== undefined) tlsOptions.pfx = agentOptions.pfx;
655+
if (agentOptions.passphrase !== undefined) tlsOptions.passphrase = agentOptions.passphrase;
656+
if (agentOptions.rejectUnauthorized !== undefined)
657+
tlsOptions.rejectUnauthorized = agentOptions.rejectUnauthorized;
658+
if ((agentOptions as any).servername !== undefined)
659+
tlsOptions.servername = (agentOptions as any).servername;
660+
661+
if (cluster && cluster.proxyUrl) {
662+
if (cluster.proxyUrl.startsWith('socks')) {
663+
return { type: 'socks', uri: cluster.proxyUrl, requestTls: tlsOptions };
664+
}
665+
if (!cluster.server.startsWith('https') && !cluster.server.startsWith('http')) {
666+
throw new Error('Unsupported proxy type');
667+
}
668+
return { type: 'proxy', uri: cluster.proxyUrl, requestTls: tlsOptions };
669+
} else if (cluster?.server?.startsWith('http:') && !cluster.skipTLSVerify) {
670+
throw new Error('HTTP protocol is not allowed when skipTLSVerify is not set or false');
671+
}
672+
if (Object.keys(tlsOptions).length === 0) {
673+
return { type: 'none' };
674+
}
675+
return { type: 'agent', connect: tlsOptions };
676+
}
677+
678+
private createDispatcher(
679+
cluster: Cluster | null,
680+
agentOptions: https.AgentOptions,
681+
): Dispatcher | undefined {
682+
const opts = this.createDispatcherOptions(cluster, agentOptions);
683+
switch (opts.type) {
684+
case 'proxy':
685+
return new UndiciProxyAgent({ uri: opts.uri, requestTls: opts.requestTls });
686+
case 'socks':
687+
return new UndiciAgent({ connect: createSocksConnector(opts.uri, opts.requestTls) });
688+
case 'agent':
689+
return new UndiciAgent({ connect: opts.connect });
690+
case 'none':
691+
return undefined;
692+
}
693+
}
694+
574695
private applyHTTPSOptions(opts: https.RequestOptions | WebSocket.ClientOptions): void {
575696
const cluster = this.getCurrentCluster();
576697
const user = this.getCurrentUser();

0 commit comments

Comments
 (0)