Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"socks-proxy-agent": "^10.0.0",
"stream-buffers": "^3.0.2",
"tar-fs": "^3.0.9",
"undici": "^8.0.0",
"undici": "^8.7.0",
"ws": "^8.18.2"
},
"devDependencies": {
Expand Down
27 changes: 27 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export class KubeConfig implements SecurityAuthentication {
// List of custom authenticators that can be added by the user
private custom_authenticators: Authenticator[] = [];

// Cache for https.Agent / proxy agent instances, keyed by JSON.stringify([clusterName, userName]).
// Reusing the same agent across WebSocket reconnections avoids opening a new TLS connection
// (and its associated file descriptor) on every reconnect for the same cluster/user pair.
private agentCache: Map<string, https.Agent | SocksProxyAgent | HttpProxyAgent | HttpsProxyAgent> =
new Map();

// Optionally add additional external authenticators, you must do this
// before you load a kubeconfig file that references them.
public addAuthenticator(authenticator: Authenticator): void {
Expand Down Expand Up @@ -585,10 +591,29 @@ export class KubeConfig implements SecurityAuthentication {
return this.getContextObject(this.currentContext);
}

/**
* Returns a stable cache key for the current cluster/user pair.
* Agents are associated with a specific cluster (TLS endpoint) and user
* (client certificate / auth), so keying on the tuple avoids opening a new
* TLS connection on every Watch reconnection or WebSocket re-attach for the
* same endpoint.
*/
private getAgentCacheKey(cluster: Cluster | null): string {
const clusterName = cluster?.name ?? '';
const userName = this.getCurrentUser()?.name ?? '';
return JSON.stringify([clusterName, userName]);
}

private createAgent(
cluster: Cluster | null,
agentOptions: https.AgentOptions,
): https.Agent | SocksProxyAgent | HttpProxyAgent | HttpsProxyAgent {
const cacheKey = this.getAgentCacheKey(cluster);
const cached = this.agentCache.get(cacheKey);
if (cached !== undefined) {
return cached;
}

let agent: https.Agent | SocksProxyAgent | HttpProxyAgent | HttpsProxyAgent;

if (cluster && cluster.proxyUrl) {
Expand All @@ -612,6 +637,8 @@ export class KubeConfig implements SecurityAuthentication {
} else {
agent = new https.Agent(agentOptions);
}

this.agentCache.set(cacheKey, agent);
return agent;
}

Expand Down
34 changes: 34 additions & 0 deletions src/config_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,40 @@ describe('KubeConfig', () => {
});
});

describe('agent and dispatcher caching', () => {
it('should return the same https.Agent instance on repeated applyToHTTPSOptions calls', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcFileName);

const opts1: https.RequestOptions = {};
const opts2: https.RequestOptions = {};
await kc.applyToHTTPSOptions(opts1);
await kc.applyToHTTPSOptions(opts2);

strictEqual(opts1.agent, opts2.agent, 'Expected the same agent instance to be reused');
});

it('should return different https.Agent instances for different cluster/user combinations', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcFileName);

// Default context uses one user
const opts1: https.RequestOptions = {};
await kc.applyToHTTPSOptions(opts1);

// Switch to a context with a different user
kc.setCurrentContext('passwd');
const opts2: https.RequestOptions = {};
await kc.applyToHTTPSOptions(opts2);

notStrictEqual(
opts1.agent,
opts2.agent,
'Expected distinct agent instances for different cluster/user pairs',
);
});
});

describe('loadClusterConfigObjects', () => {
it('should fail if name is missing from cluster', () => {
throws(
Expand Down