import "./add-cluster.scss"; import os from "os"; import React, { Fragment } from "react"; import { observer } from "mobx-react"; import { action, observable, runInAction } from "mobx"; import { remote } from "electron"; import { KubeConfig } from "@kubernetes/client-node"; import { _i18n } from "../../i18n"; import { t, Trans } from "@lingui/macro"; import { Select, SelectOption } from "../select"; import { Input } from "../input"; import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; import { WizardLayout } from "../layout/wizard-layout"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid"; import { navigate } from "../../navigation"; import { userStore } from "../../../common/user-store"; import { clusterViewURL } from "../cluster-manager/cluster-view.route"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Tab, Tabs } from "../tabs"; import { ExecValidationNotFoundError } from "../../../common/custom-errors"; import { appEventBus } from "../../../common/event-bus"; enum KubeConfigSourceTab { FILE = "file", TEXT = "text" } @observer export class AddCluster extends React.Component { @observable.ref kubeConfigLocal: KubeConfig; @observable.ref error: React.ReactNode; @observable kubeContexts = observable.map(); // available contexts from kubeconfig-file or user-input @observable selectedContexts = observable.array(); @observable sourceTab = KubeConfigSourceTab.FILE; @observable kubeConfigPath = ""; @observable customConfig = ""; @observable proxyServer = ""; @observable isWaiting = false; @observable showSettings = false; @observable dropAreaActive = false; componentDidMount() { clusterStore.setActive(null); this.setKubeConfig(userStore.kubeConfigPath); appEventBus.emit({ name: "cluster-add", action: "start" }); } componentWillUnmount() { userStore.markNewContextsAsSeen(); } @action setKubeConfig(filePath: string, { throwError = false } = {}) { try { this.kubeConfigLocal = loadConfig(filePath); validateConfig(this.kubeConfigLocal); this.refreshContexts(); this.kubeConfigPath = filePath; userStore.kubeConfigPath = filePath; // save to store } catch (err) { Notifications.error(
Can't setup {filePath} as kubeconfig: {String(err)}
); if (throwError) { throw err; } } } @action refreshContexts() { this.selectedContexts.clear(); this.kubeContexts.clear(); switch (this.sourceTab) { case KubeConfigSourceTab.FILE: const contexts = this.getContexts(this.kubeConfigLocal); this.kubeContexts.replace(contexts); break; case KubeConfigSourceTab.TEXT: try { this.error = ""; const contexts = this.getContexts(loadConfig(this.customConfig || "{}")); this.kubeContexts.replace(contexts); } catch (err) { this.error = String(err); } break; } if (this.kubeContexts.size === 1) { this.selectedContexts.push(this.kubeContexts.keys().next().value); } } getContexts(config: KubeConfig): Map { const contexts = new Map(); splitConfig(config).forEach(config => { contexts.set(config.currentContext, config); }); return contexts; } selectKubeConfigDialog = async () => { const { dialog, BrowserWindow } = remote; const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { defaultPath: this.kubeConfigPath, properties: ["openFile", "showHiddenFiles"], message: _i18n._(t`Select custom kubeconfig file`), buttonLabel: _i18n._(t`Use configuration`), }); if (!canceled && filePaths.length) { this.setKubeConfig(filePaths[0]); } }; @action addClusters = () => { let newClusters: ClusterModel[] = []; try { if (!this.selectedContexts.length) { this.error = Please select at least one cluster context; return; } this.error = ""; this.isWaiting = true; appEventBus.emit({ name: "cluster-add", action: "click" }); newClusters = this.selectedContexts.filter(context => { try { const kubeConfig = this.kubeContexts.get(context); validateKubeConfig(kubeConfig); return true; } catch (err) { this.error = String(err.message); if (err instanceof ExecValidationNotFoundError ) { Notifications.error(Error while adding cluster(s): {this.error}); return false; } else { throw new Error(err); } } }).map(context => { const clusterId = uuid(); const kubeConfig = this.kubeContexts.get(context); const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE ? this.kubeConfigPath // save link to original kubeconfig in file-system : ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder return { id: clusterId, kubeConfigPath, workspace: workspaceStore.currentWorkspaceId, contextName: kubeConfig.currentContext, preferences: { clusterName: kubeConfig.currentContext, httpsProxy: this.proxyServer || undefined, }, }; }); runInAction(() => { clusterStore.addClusters(...newClusters); if (newClusters.length === 1) { const clusterId = newClusters[0].id; clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } else { if (newClusters.length > 1) { Notifications.ok( Successfully imported {newClusters.length} cluster(s) ); } } }); this.refreshContexts(); } catch (err) { this.error = String(err); Notifications.error(Error while adding cluster(s): {this.error}); } finally { this.isWaiting = false; } }; renderInfo() { return (

Clusters associated with Lens

Add clusters by clicking the Add Cluster button. You'll need to obtain a working kubeconfig for the cluster you want to add. You can either browse it from the file system or paste it as a text from the clipboard.

Selected cluster contexts are added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.

For more information on kubeconfig see Kubernetes docs.

NOTE: Any manually added cluster is not merged into your kubeconfig file.

To see your currently enabled config with kubectl, use kubectl config view --minify --raw command in your terminal.

When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens app.

Exec auth plugins

When using exec auth plugins make sure the paths that are used to call any binaries are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, Lens app might not have all login shell env variables set automatically.

); } renderKubeConfigSource() { return ( <> Select kubeconfig file} active={this.sourceTab == KubeConfigSourceTab.FILE} /> Paste as text} active={this.sourceTab == KubeConfigSourceTab.TEXT} /> {this.sourceTab === KubeConfigSourceTab.FILE && ( <>
this.kubeConfigPath = v} onBlur={this.onKubeConfigInputBlur} /> {this.kubeConfigPath !== kubeConfigDefaultPath && ( this.setKubeConfig(kubeConfigDefaultPath)} tooltip={Reset} /> )} Browse} />
Pro-Tip: you can also drag-n-drop kubeconfig file to this area )} {this.sourceTab === KubeConfigSourceTab.TEXT && ( <> { this.customConfig = value; this.refreshContexts(); }} /> Pro-Tip: paste kubeconfig to get available contexts )} ); } renderContextSelector() { const allContexts = Array.from(this.kubeContexts.keys()); const placeholder = this.selectedContexts.length > 0 ? Selected contexts: {this.selectedContexts.length} : Select contexts; return ( <> this.proxyServer = value} theme="round-black" /> {'A HTTP proxy server URL (format: http://
:).'} )} {this.error && (
{this.error}
)}
); } }