From 30460bc9e7001cd2c0d429378076e6e6ee5c58e6 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:27:27 +0000 Subject: [PATCH 1/2] improve coverage and testability of config.ts --- src/config.ts | 29 ++++++++++++++++------- src/config_test.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/config.ts b/src/config.ts index d7687315ff..d2a4c4e38a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -394,7 +394,11 @@ export class KubeConfig implements SecurityAuthentication { this.contexts.push(ctx); } - public loadFromDefault(opts?: Partial, contextFromStartingConfig: boolean = false): void { + public loadFromDefault( + opts?: Partial, + contextFromStartingConfig: boolean = false, + platform: string = process.platform, + ): void { if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) { const files = process.env.KUBECONFIG.split(path.delimiter).filter((filename: string) => filename); this.loadFromFile(files[0], opts); @@ -405,7 +409,7 @@ export class KubeConfig implements SecurityAuthentication { } return; } - const home = findHomeDir(); + const home = findHomeDir(platform); if (home) { const config = path.join(home, '.kube', 'config'); if (fileExists(config)) { @@ -413,15 +417,15 @@ export class KubeConfig implements SecurityAuthentication { return; } } - if (process.platform === 'win32') { + if (platform === 'win32') { try { - const envKubeconfigPathResult = child_process.spawnSync('wsl.exe', [ + const envKubeconfigPathResult = this.spawnSync('wsl.exe', [ 'bash', '-c', 'printenv KUBECONFIG', ]); if (envKubeconfigPathResult.status === 0 && envKubeconfigPathResult.stdout.length > 0) { - const result = child_process.spawnSync('wsl.exe', [ + const result = this.spawnSync('wsl.exe', [ 'cat', envKubeconfigPathResult.stdout.toString('utf8'), ]); @@ -434,10 +438,10 @@ export class KubeConfig implements SecurityAuthentication { // Falling back to default kubeconfig } try { - const configResult = child_process.spawnSync('wsl.exe', ['cat', '~/.kube/config']); + const configResult = this.spawnSync('wsl.exe', ['cat', '~/.kube/config']); if (configResult.status === 0) { this.loadFromString(configResult.stdout.toString('utf8'), opts); - const result = child_process.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); + const result = this.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); if (result.status === 0) { this.makePathsAbsolute(result.stdout.toString('utf8')); } @@ -593,6 +597,13 @@ export class KubeConfig implements SecurityAuthentication { this.applyHTTPSOptions(opts); await this.applyAuthorizationHeader(opts); } + + private spawnSync( + command: string, + args: string[], + ): { status: number | null; stdout: Buffer; stderr: Buffer } { + return child_process.spawnSync(command, args); + } } export type ApiConstructor = new (config: Configuration) => T; @@ -628,8 +639,8 @@ function dropDuplicatesAndNils(a: string[]): string[] { } // Only public for testing. -export function findHomeDir(): string | null { - if (process.platform !== 'win32') { +export function findHomeDir(platform: string = process.platform): string | null { + if (platform !== 'win32') { if (process.env.HOME) { try { fs.accessSync(process.env.HOME); diff --git a/src/config_test.ts b/src/config_test.ts index 07ad255b5e..bc4556d4bd 100644 --- a/src/config_test.ts +++ b/src/config_test.ts @@ -301,8 +301,10 @@ describe('KubeConfig', () => { const kc = new KubeConfig(); kc.loadFromFile(kcTlsServerNameFileName); + const requestContext = new RequestContext('https://kube.example.com', HttpMethod.GET); const opts: https.RequestOptions = {}; await kc.applyToHTTPSOptions(opts); + await kc.applySecurityAuthentication(requestContext); const expectedAgent = new https.Agent({ ca: Buffer.from('CADATA2', 'utf-8'), @@ -322,6 +324,8 @@ describe('KubeConfig', () => { }; assertRequestOptionsEqual(opts, expectedOptions); + console.log(requestContext.getAgent()); + strictEqual((requestContext.getAgent()! as any).options.servername, 'kube.example2.com'); }); it('should apply cert configs', async () => { const kc = new KubeConfig(); @@ -1630,5 +1634,60 @@ describe('KubeConfig', () => { strictEqual(inputData!.toString(), data); mockfs.restore(); }); + it('should try to load from WSL on Windows with wsl.exe not working', () => { + const kc = new KubeConfig(); + const commands: { command: string; args: string[] }[] = []; + (kc as any).spawnSync = (cmd: string, args: string[]) => { + commands.push({ command: cmd, args }); + return { status: 1, stderr: 'some error' }; + }; + kc.loadFromDefault(undefined, false, 'win32'); + strictEqual(commands.length, 2); + for (let i = 0; i < commands.length; i++) { + strictEqual(commands[i].command, 'wsl.exe'); + } + }); + it('should try to load from WSL on Windows with $KUBECONFIG', () => { + const kc = new KubeConfig(); + const test_path = 'C:\\Users\\user\\.kube\\config'; + const configData = readFileSync(kcFileName); + const commands: { command: string; args: string[] }[] = []; + const results: { status: number; stderr: string; stdout: string }[] = [ + { status: 0, stderr: '', stdout: test_path }, + { status: 0, stderr: '', stdout: configData.toString() }, + ]; + let ix = 0; + (kc as any).spawnSync = (cmd: string, args: string[]) => { + commands.push({ command: cmd, args }); + return results[ix++]; + }; + kc.loadFromDefault(undefined, false, 'win32'); + strictEqual(commands.length, 2); + for (let i = 0; i < commands.length; i++) { + strictEqual(commands[i].command, 'wsl.exe'); + } + validateFileLoad(kc); + }); + it('should try to load from WSL on Windows without $KUBECONFIG', () => { + const kc = new KubeConfig(); + const configData = readFileSync(kcFileName); + const commands: { command: string; args: string[] }[] = []; + const results: { status: number; stderr: string; stdout: string }[] = [ + { status: 1, stderr: 'Some Error', stdout: '' }, + { status: 0, stderr: '', stdout: configData.toString() }, + { status: 0, stderr: '', stdout: 'C:\\wsldata\\.kube' }, + ]; + let ix = 0; + (kc as any).spawnSync = (cmd: string, args: string[]) => { + commands.push({ command: cmd, args }); + return results[ix++]; + }; + kc.loadFromDefault(undefined, false, 'win32'); + strictEqual(commands.length, 3); + for (let i = 0; i < commands.length; i++) { + strictEqual(commands[i].command, 'wsl.exe'); + } + validateFileLoad(kc); + }); }); }); From 3b44c174a71506d2a0e6aac36f56d5539887b1bf Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:48:04 +0000 Subject: [PATCH 2/2] Address comments --- src/config.ts | 15 ++++----------- src/config_test.ts | 15 ++++++++------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/config.ts b/src/config.ts index d2a4c4e38a..482a12a85c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -419,13 +419,13 @@ export class KubeConfig implements SecurityAuthentication { } if (platform === 'win32') { try { - const envKubeconfigPathResult = this.spawnSync('wsl.exe', [ + const envKubeconfigPathResult = child_process.spawnSync('wsl.exe', [ 'bash', '-c', 'printenv KUBECONFIG', ]); if (envKubeconfigPathResult.status === 0 && envKubeconfigPathResult.stdout.length > 0) { - const result = this.spawnSync('wsl.exe', [ + const result = child_process.spawnSync('wsl.exe', [ 'cat', envKubeconfigPathResult.stdout.toString('utf8'), ]); @@ -438,10 +438,10 @@ export class KubeConfig implements SecurityAuthentication { // Falling back to default kubeconfig } try { - const configResult = this.spawnSync('wsl.exe', ['cat', '~/.kube/config']); + const configResult = child_process.spawnSync('wsl.exe', ['cat', '~/.kube/config']); if (configResult.status === 0) { this.loadFromString(configResult.stdout.toString('utf8'), opts); - const result = this.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); + const result = child_process.spawnSync('wsl.exe', ['wslpath', '-w', '~/.kube']); if (result.status === 0) { this.makePathsAbsolute(result.stdout.toString('utf8')); } @@ -597,13 +597,6 @@ export class KubeConfig implements SecurityAuthentication { this.applyHTTPSOptions(opts); await this.applyAuthorizationHeader(opts); } - - private spawnSync( - command: string, - args: string[], - ): { status: number | null; stdout: Buffer; stderr: Buffer } { - return child_process.spawnSync(command, args); - } } export type ApiConstructor = new (config: Configuration) => T; diff --git a/src/config_test.ts b/src/config_test.ts index bc4556d4bd..9bb31dd52c 100644 --- a/src/config_test.ts +++ b/src/config_test.ts @@ -1,9 +1,11 @@ import { deepEqual, deepStrictEqual, notStrictEqual, rejects, strictEqual, throws } from 'node:assert'; +import child_process from 'node:child_process'; import { readFileSync } from 'node:fs'; import https from 'node:https'; import { Agent, RequestOptions } from 'node:https'; import path, { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { mock } from 'node:test'; import mockfs from 'mock-fs'; @@ -324,7 +326,6 @@ describe('KubeConfig', () => { }; assertRequestOptionsEqual(opts, expectedOptions); - console.log(requestContext.getAgent()); strictEqual((requestContext.getAgent()! as any).options.servername, 'kube.example2.com'); }); it('should apply cert configs', async () => { @@ -1637,10 +1638,10 @@ describe('KubeConfig', () => { it('should try to load from WSL on Windows with wsl.exe not working', () => { const kc = new KubeConfig(); const commands: { command: string; args: string[] }[] = []; - (kc as any).spawnSync = (cmd: string, args: string[]) => { + mock.method(child_process, 'spawnSync', (cmd: string, args: string[]) => { commands.push({ command: cmd, args }); return { status: 1, stderr: 'some error' }; - }; + }); kc.loadFromDefault(undefined, false, 'win32'); strictEqual(commands.length, 2); for (let i = 0; i < commands.length; i++) { @@ -1657,10 +1658,10 @@ describe('KubeConfig', () => { { status: 0, stderr: '', stdout: configData.toString() }, ]; let ix = 0; - (kc as any).spawnSync = (cmd: string, args: string[]) => { + mock.method(child_process, 'spawnSync', (cmd: string, args: string[]) => { commands.push({ command: cmd, args }); return results[ix++]; - }; + }); kc.loadFromDefault(undefined, false, 'win32'); strictEqual(commands.length, 2); for (let i = 0; i < commands.length; i++) { @@ -1678,10 +1679,10 @@ describe('KubeConfig', () => { { status: 0, stderr: '', stdout: 'C:\\wsldata\\.kube' }, ]; let ix = 0; - (kc as any).spawnSync = (cmd: string, args: string[]) => { + mock.method(child_process, 'spawnSync', (cmd: string, args: string[]) => { commands.push({ command: cmd, args }); return results[ix++]; - }; + }); kc.loadFromDefault(undefined, false, 'win32'); strictEqual(commands.length, 3); for (let i = 0; i < commands.length; i++) {