diff --git a/src/config.ts b/src/config.ts index b625642c41..a9dd9982b8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,11 +36,19 @@ import WebSocket from 'isomorphic-ws'; import child_process from 'node:child_process'; import { SocksProxyAgent } from 'socks-proxy-agent'; import { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, HttpsProxyAgentOptions } from 'hpagent'; +import packagejson from '../package.json' with { type: 'json' }; +import { setHeaderMiddleware } from './middleware.js'; 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'; const SERVICEACCOUNT_NAMESPACE_PATH: string = SERVICEACCOUNT_ROOT + '/namespace'; +const USER_AGENT_KEY = 'User-Agent'; + +function getUserAgent(): string { + const version = packagejson.version ?? ''; + return `kubernetes-client-javascript/${version}`; +} // fs.existsSync was removed in node 10 function fileExists(filepath: string): boolean { @@ -491,6 +499,7 @@ export class KubeConfig implements SecurityAuthentication { const config: Configuration = createConfiguration({ baseServer: baseServerConfig, authMethods: authConfig, + middleware: [setHeaderMiddleware(USER_AGENT_KEY, getUserAgent())], }); const apiClient = new apiClientType(config); diff --git a/src/config_test.ts b/src/config_test.ts index aab45ebbfe..0e4dc27fd3 100644 --- a/src/config_test.ts +++ b/src/config_test.ts @@ -1,4 +1,4 @@ -import { after, before, beforeEach, describe, it, mock } from 'node:test'; +import { after, before, beforeEach, describe, it, mock, TestContext } from 'node:test'; import assert, { deepEqual, deepStrictEqual, @@ -1674,6 +1674,67 @@ describe('KubeConfig', () => { const client = kc.makeApiClient(CoreV1Api); strictEqual(client instanceof CoreV1Api, true); }); + + it('should include User-Agent header with version', async (t: TestContext) => { + let capturedUserAgent: string | undefined; + + const { server, host, port } = await createTestHttpsServer((req, res) => { + capturedUserAgent = req.headers['user-agent']; + + res.setHeader('Content-Type', 'application/json'); + res.writeHead(200); + res.end( + JSON.stringify({ + apiVersion: 'v1', + kind: 'NamespaceList', + items: [], + }), + ); + }); + t.after(async () => { + await new Promise((resolve) => { + server.close(resolve); + }); + }); + + const kc = new KubeConfig(); + kc.loadFromClusterAndUser( + { + name: 'test-cluster', + server: `https://${host}:${port}`, + skipTLSVerify: true, + } as Cluster, + { + name: 'test-user', + token: 'test-token', + } as User, + ); + + const coreV1Api = kc.makeApiClient(CoreV1Api); + await coreV1Api.listNamespace(); + + // Read version from package.json + const packageJsonPath = join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const expectedVersion = packageJson.version; + + // Verify version is not blank + strictEqual(typeof expectedVersion, 'string'); + strictEqual(expectedVersion.length > 0, true, 'package.json version should not be blank'); + + // Verify User-Agent header contains client name and version + strictEqual(typeof capturedUserAgent, 'string'); + strictEqual( + capturedUserAgent!.startsWith('kubernetes-client-javascript/'), + true, + 'capturedUserAgent should start with "kubernetes-javascript-client/"', + ); + strictEqual( + capturedUserAgent!.endsWith(expectedVersion), + true, + `User-Agent should include version ${expectedVersion}`, + ); + }); }); describe('EmptyConfig', () => {