Skip to content

Commit c304f01

Browse files
committed
Support KUBECONFIG environment variable
- Watch for changes in file(s) indicated by `KUBECONFIG` when it's set - Force users to pick one file when multiple config files are specified in `KUBECONFIG`, since the Kubernetes client library we are using doesn't support using multiple files Closes #3382 Signed-off-by: David Thompson <[email protected]>
1 parent fd91d9b commit c304f01

File tree

6 files changed

+81
-35
lines changed

6 files changed

+81
-35
lines changed

src/explorer.ts

+25-25
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ import {
2424
import * as Helm from './helm/helm';
2525
import { Oc } from './oc/ocWrapper';
2626
import { Odo } from './odo/odoWrapper';
27-
import { KubeConfigUtils } from './util/kubeUtils';
27+
import { KubeConfigUtils, getKubeConfigFiles } from './util/kubeUtils';
2828
import { Platform } from './util/platform';
2929
import { Progress } from './util/progress';
3030
import { FileContentChangeNotifier, WatchUtil } from './util/watch';
3131
import { vsCommand } from './vscommand';
3232

33-
const kubeConfigFolder: string = path.join(Platform.getUserHomePath(), '.kube');
34-
3533
type ExplorerItem = KubernetesObject | Helm.HelmRelease | Context | TreeItem;
3634

3735
type PackageJSON = {
@@ -42,7 +40,7 @@ type PackageJSON = {
4240
const CREATE_OR_SET_PROJECT_ITEM = {
4341
label: 'Create new or set active Project',
4442
command: {
45-
title: 'Create new or ser active Project',
43+
title: 'Create new or set active Project',
4644
command: 'openshift.project.set'
4745
}
4846
};
@@ -52,7 +50,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
5250

5351
private treeView: TreeView<ExplorerItem>;
5452

55-
private fsw: FileContentChangeNotifier;
53+
private kubeConfigWatchers: FileContentChangeNotifier[];
5654
private kubeContext: Context;
5755
private kubeConfig: KubeConfigUtils;
5856

@@ -70,22 +68,25 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
7068
// ignore config loading error and let odo report it on first call
7169
}
7270
try {
73-
this.fsw = WatchUtil.watchFileForContextChange(kubeConfigFolder, 'config');
71+
const kubeconfigFiles = getKubeConfigFiles();
72+
this.kubeConfigWatchers = kubeconfigFiles.map(kubeconfigFile => WatchUtil.watchFileForContextChange(path.dirname(kubeconfigFile), path.basename(kubeconfigFile)));
7473
} catch (err) {
7574
void window.showWarningMessage('Couldn\'t install watcher for Kubernetes configuration file. OpenShift Application Explorer view won\'t be updated automatically.');
7675
}
77-
this.fsw?.emitter?.on('file-changed', () => {
78-
const ku2 = new KubeConfigUtils();
79-
const newCtx = ku2.getContextObject(ku2.currentContext);
80-
if (!this.kubeContext
81-
|| (this.kubeContext.cluster !== newCtx.cluster
82-
|| this.kubeContext.user !== newCtx.user
83-
|| this.kubeContext.namespace !== newCtx.namespace)) {
84-
this.refresh();
85-
}
86-
this.kubeContext = newCtx;
87-
this.kubeConfig = ku2;
88-
});
76+
for (const fsw of this.kubeConfigWatchers) {
77+
fsw.emitter?.on('file-changed', () => {
78+
const ku2 = new KubeConfigUtils();
79+
const newCtx = ku2.getContextObject(ku2.currentContext);
80+
if (Boolean(this.kubeContext) !== Boolean(newCtx)
81+
|| (this.kubeContext.cluster !== newCtx.cluster
82+
|| this.kubeContext.user !== newCtx.user
83+
|| this.kubeContext.namespace !== newCtx.namespace)) {
84+
this.refresh();
85+
}
86+
this.kubeContext = newCtx;
87+
this.kubeConfig = ku2;
88+
});
89+
}
8990
this.treeView = window.createTreeView<ExplorerItem>('openshiftProjectExplorer', {
9091
treeDataProvider: this,
9192
});
@@ -110,7 +111,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
110111
contextValue: 'openshift.openConfigFile',
111112
label: element.label,
112113
collapsibleState: TreeItemCollapsibleState.None,
113-
tooltip: 'Default KubeConfig',
114+
tooltip: element.label as string,
114115
description: element.description,
115116
iconPath: new ThemeIcon('file')
116117
};
@@ -175,11 +176,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
175176
await Odo.Instance.getProjects();
176177
result = [this.kubeContext];
177178
if (this.kubeContext) {
178-
const homeDir = this.kubeConfig.findHomeDir();
179-
if (homeDir){
180-
const config = path.join(homeDir, '.kube', 'config');
181-
result.unshift({label: 'Default KubeConfig', description: `${config}`})
182-
}
179+
const config = getKubeConfigFiles();
180+
result.unshift({label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':')})
183181
}
184182
} catch (err) {
185183
// ignore because ether server is not accessible or user is logged out
@@ -246,7 +244,9 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
246244
}
247245

248246
dispose(): void {
249-
this.fsw?.watcher?.close();
247+
for (const fsw of this.kubeConfigWatchers) {
248+
fsw?.watcher?.close();
249+
}
250250
this.treeView.dispose();
251251
}
252252

src/extension.ts

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ServerlessFunctionView } from './serverlessFunction/view';
2525
import { startTelemetry } from './telemetry';
2626
import { ToolsConfig } from './tools';
2727
import { TokenStore } from './util/credentialManager';
28+
import { setKubeConfig } from './util/kubeUtils';
2829
import { Platform } from './util/platform';
2930
import { setupWorkspaceDevfileContext } from './util/workspace';
3031
import { registerCommands } from './vscommand';
@@ -71,6 +72,10 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
7172
migrateFromOdo018();
7273
Cluster.extensionContext = extensionContext;
7374
TokenStore.extensionContext = extensionContext;
75+
76+
// pick kube config in case multiple are configured
77+
await setKubeConfig();
78+
7479
const crcStatusItem = window.createStatusBarItem(StatusBarAlignment.Left);
7580
crcStatusItem.command = 'openshift.explorer.stopCluster';
7681
const disposable = [

src/openshift/cluster.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { KubernetesObject } from '@kubernetes/client-node';
77
import { ExtensionContext, InputBox, QuickInputButton, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, commands, env, window, workspace } from 'vscode';
88
import { CommandText } from '../base/command';
99
import { CliChannel } from '../cli';
10+
import { OpenShiftExplorer } from '../explorer';
1011
import { Oc } from '../oc/ocWrapper';
1112
import { Command } from '../odo/command';
1213
import { Odo } from '../odo/odoWrapper';
@@ -48,7 +49,7 @@ export class Cluster extends OpenShiftItem {
4849
),
4950
)
5051
.then(async () => {
51-
Cluster.explorer.refresh();
52+
OpenShiftExplorer.getInstance().refresh();
5253
Cluster.serverlessView.refresh();
5354
void commands.executeCommand('setContext', 'isLoggedIn', false);
5455
const logoutInfo = await window.showInformationMessage(
@@ -67,7 +68,7 @@ export class Cluster extends OpenShiftItem {
6768

6869
@vsCommand('openshift.explorer.refresh')
6970
static refresh(): void {
70-
Cluster.explorer.refresh();
71+
OpenShiftExplorer.getInstance().refresh();
7172
Cluster.serverlessView.refresh();
7273
}
7374

@@ -806,7 +807,7 @@ export class Cluster extends OpenShiftItem {
806807
}
807808

808809
static async loginMessage(clusterURL: string): Promise<string> {
809-
Cluster.explorer.refresh();
810+
OpenShiftExplorer.getInstance().refresh();
810811
Cluster.serverlessView.refresh();
811812
await commands.executeCommand('setContext', 'isLoggedIn', true);
812813
return `Successfully logged in to '${clusterURL}'`;

src/openshift/openshiftItem.ts

-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*-----------------------------------------------------------------------------------------------*/
55

66
import { commands, QuickPickItem, window } from 'vscode';
7-
import { OpenShiftExplorer } from '../explorer';
87
import { Oc } from '../oc/ocWrapper';
98
import { Odo } from '../odo/odoWrapper';
109
import { Project } from '../odo/project';
@@ -26,8 +25,6 @@ export class QuickPickCommand implements QuickPickItem {
2625
export default class OpenShiftItem {
2726
protected static readonly odo = Odo.Instance;
2827

29-
protected static readonly explorer: OpenShiftExplorer = OpenShiftExplorer.getInstance();
30-
3128
protected static readonly serverlessView: ServerlessFunctionView = ServerlessFunctionView.getInstance();
3229

3330
static async getName(message: string, offset?: string, defaultValue = ''): Promise<string> {

src/openshift/project.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Project extends OpenShiftItem {
3737
} else {
3838
const projectName = selectedItem.label;
3939
await Odo.Instance.setProject(projectName);
40-
Project.explorer.refresh();
40+
OpenShiftExplorer.getInstance().refresh();
4141
Project.serverlessView.refresh();
4242
message = `Project '${projectName}' set as active.`;
4343
}

src/util/kubeUtils.ts

+46-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6+
import { KubeConfig, findHomeDir, loadYaml } from '@kubernetes/client-node';
7+
import { Cluster, User } from '@kubernetes/client-node/dist/config_types';
68
import * as fs from 'fs';
79
import * as path from 'path';
8-
import { QuickPickItem } from 'vscode';
9-
import { KubeConfig, findHomeDir, loadYaml } from '@kubernetes/client-node';
10-
import { User, Cluster } from '@kubernetes/client-node/dist/config_types';
10+
import { QuickPickItem, window } from 'vscode';
11+
import { Platform } from './platform';
1112

1213
function fileExists(file: string): boolean {
1314
try {
@@ -102,3 +103,45 @@ export class KubeConfigUtils extends KubeConfig {
102103
}
103104

104105
}
106+
107+
/**
108+
* Returns the list of kube config files:
109+
* - If KUBECONFIG is not set, just ~/.kube/config
110+
* - If KUBECONFIG is set, follows the semantics for specifying multiple config files described here:
111+
* https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/#append-home-kube-config-to-your-kubeconfig-environment-variable
112+
* BUT: it shows an error if multiple configs are specified, since the Kubernetes client we are using doesn't support this use case.
113+
*/
114+
export function getKubeConfigFiles(): string[] {
115+
if (process.env.KUBECONFIG) {
116+
const configuredFiles: string[] = process.env.KUBECONFIG.split(path.delimiter);
117+
const filesThatExist: string[] = [];
118+
for (const configFile of configuredFiles) {
119+
if (fs.existsSync(configFile)) {
120+
filesThatExist.push(configFile);
121+
}
122+
}
123+
return filesThatExist;
124+
}
125+
return [path.join(Platform.getUserHomePath(), '.kube', 'config')];
126+
}
127+
128+
/**
129+
* If there are multiple kube config files set, force the user to pick one to use.
130+
*/
131+
export async function setKubeConfig(): Promise<void> {
132+
const kubeConfigFiles = getKubeConfigFiles();
133+
if (kubeConfigFiles.length > 1) {
134+
let selectedFile;
135+
while(!selectedFile) {
136+
try {
137+
const potentialSelection = await window.showQuickPick(kubeConfigFiles, { canPickMany: false, placeHolder: 'VSCode OpenShift only supports using one kube config. Please select which one to use.' });
138+
if (potentialSelection) {
139+
selectedFile = potentialSelection;
140+
}
141+
} catch (_) {
142+
// do nothing
143+
}
144+
}
145+
process.env.KUBECONFIG = selectedFile;
146+
}
147+
}

0 commit comments

Comments
 (0)