diff --git a/package.json b/package.json index ea65b9d2f0..43c5c31359 100644 --- a/package.json +++ b/package.json @@ -175,6 +175,7 @@ "@types/tar": "^4.0.3", "array-move": "^3.0.0", "chalk": "^4.1.0", + "command-exists": "1.2.9", "conf": "^7.0.1", "crypto-js": "^4.0.0", "electron-updater": "^4.3.1", diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts new file mode 100644 index 0000000000..3c7750487a --- /dev/null +++ b/src/common/custom-errors.ts @@ -0,0 +1,12 @@ +export class ExecValidationNotFoundError extends Error { + constructor(execPath: string, isAbsolute: boolean) { + super(`User Exec command "${execPath}" not found on host.`); + let message = `User Exec command "${execPath}" not found on host.`; + if (!isAbsolute) { + message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`; + } + this.message = message; + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index 18f472243b..8f517ba555 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -4,6 +4,8 @@ import path from "path" import os from "os" import yaml from "js-yaml" import logger from "../main/logger"; +import commandExists from "command-exists"; +import { ExecValidationNotFoundError } from "./custom-errors"; export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config'); @@ -140,3 +142,28 @@ export function getNodeWarningConditions(node: V1Node) { c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" ) } + +/** + * Validates kubeconfig supplied in the add clusters screen. At present this will just validate + * the User struct, specifically the command passed to the exec substructure. + */ +export function validateKubeConfig (config: KubeConfig) { + // we only receive a single context, cluster & user object here so lets validate them as this + // will be called when we add a new cluster to Lens + logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`); + + // Validate the User Object + const user = config.getCurrentUser(); + if (user.exec) { + const execCommand = user.exec["command"]; + // check if the command is absolute or not + const isAbsolute = path.isAbsolute(execCommand); + // validate the exec struct in the user object, start with the command field + logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`); + + if (!commandExists.sync(execCommand)) { + logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`); + throw new ExecValidationNotFoundError(execCommand, isAbsolute); + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index a0414541d8..60d609d4f0 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -13,7 +13,7 @@ import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; import { WizardLayout } from "../layout/wizard-layout"; -import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig } from "../../../common/kube-helpers"; +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" @@ -23,6 +23,7 @@ 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"; enum KubeConfigSourceTab { FILE = "file", @@ -118,14 +119,32 @@ export class AddCluster extends React.Component { } addClusters = () => { + const configValidationErrors:string[] = []; + let newClusters: ClusterModel[] = []; + try { if (!this.selectedContexts.length) { this.error = Please select at least one cluster context return; } this.error = "" - this.isWaiting = true - const newClusters: ClusterModel[] = this.selectedContexts.map(context => { + this.isWaiting = true + + 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 @@ -141,7 +160,8 @@ export class AddCluster extends React.Component { httpsProxy: this.proxyServer || undefined, }, } - }); + }) + runInAction(() => { clusterStore.addCluster(...newClusters); if (newClusters.length === 1) { @@ -149,9 +169,11 @@ export class AddCluster extends React.Component { clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } else { - Notifications.ok( - Successfully imported {newClusters.length} cluster(s) - ); + if (newClusters.length > 1) { + Notifications.ok( + Successfully imported {newClusters.length} cluster(s) + ); + } } }) this.refreshContexts(); diff --git a/types/command-exists.d.ts b/types/command-exists.d.ts new file mode 100644 index 0000000000..b5375ae390 --- /dev/null +++ b/types/command-exists.d.ts @@ -0,0 +1,16 @@ +// Type definitions for command-exists 1.2 +// Project: https://github.com/mathisonian/command-exists +// Definitions by: BendingBender +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +export = commandExists; + +declare function commandExists(commandName: string): Promise; +declare function commandExists( + commandName: string, + cb: (error: null, exists: boolean) => void +): void; + +declare namespace commandExists { + function sync(commandName: string): boolean; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1c92dd0978..a8dcc87642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3888,6 +3888,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +command-exists@1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + commander@*, commander@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"