Skip to content

Commit

Permalink
Support KUBECONFIG environment variable
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
datho7561 committed Oct 20, 2023
1 parent fd91d9b commit c304f01
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 35 deletions.
50 changes: 25 additions & 25 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@ import {
import * as Helm from './helm/helm';
import { Oc } from './oc/ocWrapper';
import { Odo } from './odo/odoWrapper';
import { KubeConfigUtils } from './util/kubeUtils';
import { KubeConfigUtils, getKubeConfigFiles } from './util/kubeUtils';
import { Platform } from './util/platform';
import { Progress } from './util/progress';
import { FileContentChangeNotifier, WatchUtil } from './util/watch';
import { vsCommand } from './vscommand';

const kubeConfigFolder: string = path.join(Platform.getUserHomePath(), '.kube');

type ExplorerItem = KubernetesObject | Helm.HelmRelease | Context | TreeItem;

type PackageJSON = {
Expand All @@ -42,7 +40,7 @@ type PackageJSON = {
const CREATE_OR_SET_PROJECT_ITEM = {
label: 'Create new or set active Project',
command: {
title: 'Create new or ser active Project',
title: 'Create new or set active Project',
command: 'openshift.project.set'
}
};
Expand All @@ -52,7 +50,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos

private treeView: TreeView<ExplorerItem>;

private fsw: FileContentChangeNotifier;
private kubeConfigWatchers: FileContentChangeNotifier[];
private kubeContext: Context;
private kubeConfig: KubeConfigUtils;

Expand All @@ -70,22 +68,25 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
// ignore config loading error and let odo report it on first call
}
try {
this.fsw = WatchUtil.watchFileForContextChange(kubeConfigFolder, 'config');
const kubeconfigFiles = getKubeConfigFiles();
this.kubeConfigWatchers = kubeconfigFiles.map(kubeconfigFile => WatchUtil.watchFileForContextChange(path.dirname(kubeconfigFile), path.basename(kubeconfigFile)));
} catch (err) {
void window.showWarningMessage('Couldn\'t install watcher for Kubernetes configuration file. OpenShift Application Explorer view won\'t be updated automatically.');
}
this.fsw?.emitter?.on('file-changed', () => {
const ku2 = new KubeConfigUtils();
const newCtx = ku2.getContextObject(ku2.currentContext);
if (!this.kubeContext
|| (this.kubeContext.cluster !== newCtx.cluster
|| this.kubeContext.user !== newCtx.user
|| this.kubeContext.namespace !== newCtx.namespace)) {
this.refresh();
}
this.kubeContext = newCtx;
this.kubeConfig = ku2;
});
for (const fsw of this.kubeConfigWatchers) {
fsw.emitter?.on('file-changed', () => {
const ku2 = new KubeConfigUtils();
const newCtx = ku2.getContextObject(ku2.currentContext);
if (Boolean(this.kubeContext) !== Boolean(newCtx)
|| (this.kubeContext.cluster !== newCtx.cluster
|| this.kubeContext.user !== newCtx.user
|| this.kubeContext.namespace !== newCtx.namespace)) {
this.refresh();
}
this.kubeContext = newCtx;
this.kubeConfig = ku2;
});
}
this.treeView = window.createTreeView<ExplorerItem>('openshiftProjectExplorer', {
treeDataProvider: this,
});
Expand All @@ -110,7 +111,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
contextValue: 'openshift.openConfigFile',
label: element.label,
collapsibleState: TreeItemCollapsibleState.None,
tooltip: 'Default KubeConfig',
tooltip: element.label as string,
description: element.description,
iconPath: new ThemeIcon('file')
};
Expand Down Expand Up @@ -175,11 +176,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
await Odo.Instance.getProjects();
result = [this.kubeContext];
if (this.kubeContext) {
const homeDir = this.kubeConfig.findHomeDir();
if (homeDir){
const config = path.join(homeDir, '.kube', 'config');
result.unshift({label: 'Default KubeConfig', description: `${config}`})
}
const config = getKubeConfigFiles();
result.unshift({label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':')})
}
} catch (err) {
// ignore because ether server is not accessible or user is logged out
Expand Down Expand Up @@ -246,7 +244,9 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
}

dispose(): void {
this.fsw?.watcher?.close();
for (const fsw of this.kubeConfigWatchers) {
fsw?.watcher?.close();
}
this.treeView.dispose();
}

Expand Down
5 changes: 5 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ServerlessFunctionView } from './serverlessFunction/view';
import { startTelemetry } from './telemetry';
import { ToolsConfig } from './tools';
import { TokenStore } from './util/credentialManager';
import { setKubeConfig } from './util/kubeUtils';
import { Platform } from './util/platform';
import { setupWorkspaceDevfileContext } from './util/workspace';
import { registerCommands } from './vscommand';
Expand Down Expand Up @@ -71,6 +72,10 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
migrateFromOdo018();
Cluster.extensionContext = extensionContext;
TokenStore.extensionContext = extensionContext;

// pick kube config in case multiple are configured
await setKubeConfig();

const crcStatusItem = window.createStatusBarItem(StatusBarAlignment.Left);
crcStatusItem.command = 'openshift.explorer.stopCluster';
const disposable = [
Expand Down
7 changes: 4 additions & 3 deletions src/openshift/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { KubernetesObject } from '@kubernetes/client-node';
import { ExtensionContext, InputBox, QuickInputButton, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, commands, env, window, workspace } from 'vscode';
import { CommandText } from '../base/command';
import { CliChannel } from '../cli';
import { OpenShiftExplorer } from '../explorer';
import { Oc } from '../oc/ocWrapper';
import { Command } from '../odo/command';
import { Odo } from '../odo/odoWrapper';
Expand Down Expand Up @@ -48,7 +49,7 @@ export class Cluster extends OpenShiftItem {
),
)
.then(async () => {
Cluster.explorer.refresh();
OpenShiftExplorer.getInstance().refresh();
Cluster.serverlessView.refresh();
void commands.executeCommand('setContext', 'isLoggedIn', false);
const logoutInfo = await window.showInformationMessage(
Expand All @@ -67,7 +68,7 @@ export class Cluster extends OpenShiftItem {

@vsCommand('openshift.explorer.refresh')
static refresh(): void {
Cluster.explorer.refresh();
OpenShiftExplorer.getInstance().refresh();
Cluster.serverlessView.refresh();
}

Expand Down Expand Up @@ -806,7 +807,7 @@ export class Cluster extends OpenShiftItem {
}

static async loginMessage(clusterURL: string): Promise<string> {
Cluster.explorer.refresh();
OpenShiftExplorer.getInstance().refresh();
Cluster.serverlessView.refresh();
await commands.executeCommand('setContext', 'isLoggedIn', true);
return `Successfully logged in to '${clusterURL}'`;
Expand Down
3 changes: 0 additions & 3 deletions src/openshift/openshiftItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*-----------------------------------------------------------------------------------------------*/

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

protected static readonly explorer: OpenShiftExplorer = OpenShiftExplorer.getInstance();

protected static readonly serverlessView: ServerlessFunctionView = ServerlessFunctionView.getInstance();

static async getName(message: string, offset?: string, defaultValue = ''): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion src/openshift/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class Project extends OpenShiftItem {
} else {
const projectName = selectedItem.label;
await Odo.Instance.setProject(projectName);
Project.explorer.refresh();
OpenShiftExplorer.getInstance().refresh();
Project.serverlessView.refresh();
message = `Project '${projectName}' set as active.`;
}
Expand Down
49 changes: 46 additions & 3 deletions src/util/kubeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*-----------------------------------------------------------------------------------------------*/

import { KubeConfig, findHomeDir, loadYaml } from '@kubernetes/client-node';
import { Cluster, User } from '@kubernetes/client-node/dist/config_types';
import * as fs from 'fs';
import * as path from 'path';
import { QuickPickItem } from 'vscode';
import { KubeConfig, findHomeDir, loadYaml } from '@kubernetes/client-node';
import { User, Cluster } from '@kubernetes/client-node/dist/config_types';
import { QuickPickItem, window } from 'vscode';
import { Platform } from './platform';

function fileExists(file: string): boolean {
try {
Expand Down Expand Up @@ -102,3 +103,45 @@ export class KubeConfigUtils extends KubeConfig {
}

}

/**
* Returns the list of kube config files:
* - If KUBECONFIG is not set, just ~/.kube/config
* - If KUBECONFIG is set, follows the semantics for specifying multiple config files described here:
* https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/#append-home-kube-config-to-your-kubeconfig-environment-variable
* BUT: it shows an error if multiple configs are specified, since the Kubernetes client we are using doesn't support this use case.
*/
export function getKubeConfigFiles(): string[] {
if (process.env.KUBECONFIG) {
const configuredFiles: string[] = process.env.KUBECONFIG.split(path.delimiter);
const filesThatExist: string[] = [];
for (const configFile of configuredFiles) {
if (fs.existsSync(configFile)) {
filesThatExist.push(configFile);
}
}
return filesThatExist;
}
return [path.join(Platform.getUserHomePath(), '.kube', 'config')];
}

/**
* If there are multiple kube config files set, force the user to pick one to use.
*/
export async function setKubeConfig(): Promise<void> {
const kubeConfigFiles = getKubeConfigFiles();
if (kubeConfigFiles.length > 1) {
let selectedFile;
while(!selectedFile) {
try {
const potentialSelection = await window.showQuickPick(kubeConfigFiles, { canPickMany: false, placeHolder: 'VSCode OpenShift only supports using one kube config. Please select which one to use.' });
if (potentialSelection) {
selectedFile = potentialSelection;
}
} catch (_) {
// do nothing
}
}
process.env.KUBECONFIG = selectedFile;
}
}

0 comments on commit c304f01

Please sign in to comment.