From 4fcac6b0d004d3805863b03526771ce5b13aee0b Mon Sep 17 00:00:00 2001 From: steve richards Date: Mon, 12 Oct 2020 08:20:08 +0100 Subject: [PATCH] Added additional checks on the command used in the Exec plugin in a kubeconfig (#1013) - Added check to see if the program being referenced in the command field of the exec object in the User construct exists. If it doesn't an error will be raised. If more than 1 context is selected when adding a kubeconfig then valid contexts will be added and any with an error will not be. Signed-off-by: Steve Richards Co-authored-by: Steve Richards --- package.json | 1 + src/common/custom-errors.ts | 12 +++++++ src/common/kube-helpers.ts | 27 ++++++++++++++ .../components/+add-cluster/add-cluster.tsx | 36 +++++++++++++++---- types/command-exists.d.ts | 16 +++++++++ yarn.lock | 5 +++ 6 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/common/custom-errors.ts create mode 100644 types/command-exists.d.ts 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"