Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 20 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,11 @@ export class KubeConfig implements SecurityAuthentication {
this.contexts.push(ctx);
}

public loadFromDefault(opts?: Partial<ConfigOptions>, contextFromStartingConfig: boolean = false): void {
public loadFromDefault(
opts?: Partial<ConfigOptions>,
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);
Expand All @@ -405,23 +409,23 @@ 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)) {
this.loadFromFile(config, opts);
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'),
]);
Expand All @@ -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'));
}
Expand Down Expand Up @@ -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<T extends ApiType> = new (config: Configuration) => T;
Expand Down Expand Up @@ -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);
Expand Down
59 changes: 59 additions & 0 deletions src/config_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -322,6 +324,8 @@ describe('KubeConfig', () => {
};

assertRequestOptionsEqual(opts, expectedOptions);
console.log(requestContext.getAgent());
strictEqual((requestContext.getAgent()! as any).options.servername, 'kube.example2.com');
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this rather an https.Agent than an any?
This would remove the unnecessary type casting but you probably still need the bang-operator or a different check.
Not sure if it's worth it, just wanted to mention that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

options isn't exposed as public (at least not in the typings), so Typescript doesn't like it. The cast as any is mostly to get the typescript compiler to leave it alone.

});
it('should apply cert configs', async () => {
const kc = new KubeConfig();
Expand Down Expand Up @@ -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[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to change this PR, but just something for future consideration - all supported Node release lines do have this type of mocking and spying built in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out, it didn't show up when I looked around for mocking child_process but now I found it and I refactored the test to use this instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, I'm actually surprised that the CI is passing. Mocking an ES module like that should not work because they have immutable bindings. When I pull this PR down locally, these tests fail.

The approach here would work with the previous state of the PR though, where you were mocking the KubeConfig object's method. I think we should go back to that.

ESM mocking is quite the can of worms. There is support for that in newer versions of Node (and some ecosystem modules as well), but they build on top of Node's ESM loaders, which have been extremely unstable and are still experimental.

Copy link
Contributor

Choose a reason for hiding this comment

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

I dug into this a bit more. The mock is surprisingly working. The reason the tests are failing on my local machine is that loadFromDefault() is returning early here, so the mocked spawnSync() calls never happen.

Copy link
Contributor

@cjihrig cjihrig Mar 5, 2025

Choose a reason for hiding this comment

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

This gets the new tests passing for me locally:

diff --git a/src/config_test.ts b/src/config_test.ts
index 9bb31dd5..30f4d49f 100644
--- a/src/config_test.ts
+++ b/src/config_test.ts
@@ -1613,6 +1613,17 @@ describe('KubeConfig', () => {
     });
 
     describe('BufferOrFile', () => {
+        let originalEnv;
+
+        before(() => {
+            originalEnv = process.env;
+            process.env = {};
+        });
+
+        after(() => {
+            process.env = originalEnv;
+        });
+
         it('should load from root if present', () => {
             const data = 'some data for file';
             const arg: any = {

Looking at the code, mocking fs.accessSync() to throw might also get the job done.

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);
});
});
});