diff --git a/package.json b/package.json index 13f2d355e8..5a6d205087 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,6 @@ "byline": "^5.0.0", "chalk": "^4.1.0", "chokidar": "^3.4.3", - "command-exists": "1.2.9", "conf": "^7.1.2", "crypto-js": "^4.1.1", "electron-devtools-installer": "^3.2.0", diff --git a/src/common/__tests__/kube-helpers.test.ts b/src/common/__tests__/kube-helpers.test.ts index 5d2bd35344..277398ee02 100644 --- a/src/common/__tests__/kube-helpers.test.ts +++ b/src/common/__tests__/kube-helpers.test.ts @@ -20,7 +20,7 @@ */ import { KubeConfig } from "@kubernetes/client-node"; -import { validateKubeConfig, loadConfigFromString, getNodeWarningConditions } from "../kube-helpers"; +import { validateKubeConfig, loadConfigFromString } from "../kube-helpers"; const kubeconfig = ` apiVersion: v1 @@ -120,38 +120,6 @@ describe("kube helpers", () => { ); }); }); - - describe("with invalid exec command", () => { - it("returns an error", () => { - expect(String(validateKubeConfig(kc, "invalidExec"))).toEqual( - expect.stringContaining("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig") - ); - }); - }); - }); - - describe("with validateCluster as false", () => { - describe("with invalid cluster object", () => { - it("does not return an error", () => { - expect(validateKubeConfig(kc, "invalidCluster", { validateCluster: false })).toBeUndefined(); - }); - }); - }); - - describe("with validateUser as false", () => { - describe("with invalid user object", () => { - it("does not return an error", () => { - expect(validateKubeConfig(kc, "invalidUser", { validateUser: false })).toBeUndefined(); - }); - }); - }); - - describe("with validateExec as false", () => { - describe("with invalid exec object", () => { - it("does not return an error", () => { - expect(validateKubeConfig(kc, "invalidExec", { validateExec: false })).toBeUndefined(); - }); - }); }); }); @@ -280,43 +248,4 @@ describe("kube helpers", () => { }); }); }); - - describe("getNodeWarningConditions", () => { - it("should return an empty array if no status or no conditions", () => { - expect(getNodeWarningConditions({}).length).toBe(0); - }); - - it("should return an empty array if all conditions are good", () => { - expect(getNodeWarningConditions({ - status: { - conditions: [ - { - type: "Ready", - status: "foobar" - } - ] - } - }).length).toBe(0); - }); - - it("should all not ready conditions", () => { - const conds = getNodeWarningConditions({ - status: { - conditions: [ - { - type: "Ready", - status: "foobar" - }, - { - type: "NotReady", - status: "true" - }, - ] - } - }); - - expect(conds.length).toBe(1); - expect(conds[0].type).toBe("NotReady"); - }); - }); }); diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts deleted file mode 100644 index 2cbc75ccf0..0000000000 --- a/src/common/custom-errors.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -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); - } -} diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index aff05e68c6..16046b4676 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -19,24 +19,16 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"; +import { KubeConfig } from "@kubernetes/client-node"; import fse from "fs-extra"; 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"; import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types"; import { resolvePath } from "./utils"; import Joi from "joi"; -export type KubeConfigValidationOpts = { - validateCluster?: boolean; - validateUser?: boolean; - validateExec?: boolean; -}; - export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config"); export function loadConfigFromFileSync(filePath: string): ConfigResult { @@ -86,35 +78,34 @@ const contextSchema = Joi.object({ }), }); -const kubeConfigSchema = Joi - .object({ - users: Joi - .array() - .items(userSchema) - .optional(), - clusters: Joi - .array() - .items(clusterSchema) - .optional(), - contexts: Joi - .array() - .items(contextSchema) - .optional(), - "current-context": Joi - .string() - .min(1) - .optional(), - }) +const kubeConfigSchema = Joi.object({ + users: Joi + .array() + .items(userSchema) + .optional(), + clusters: Joi + .array() + .items(clusterSchema) + .optional(), + contexts: Joi + .array() + .items(contextSchema) + .optional(), + "current-context": Joi + .string() + .min(1) + .optional(), +}) .required(); -export interface KubeConfigOptions { +interface KubeConfigOptions { clusters: Cluster[]; users: User[]; contexts: Context[]; currentContext?: string; } -export interface OptionsResult { +interface OptionsResult { options: KubeConfigOptions; error: Joi.ValidationError; } @@ -132,7 +123,12 @@ function loadToOptions(rawYaml: string): OptionsResult { arrays: true, } }); - const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = value ?? {}; + const { + clusters: rawClusters, + users: rawUsers, + contexts: rawContexts, + "current-context": currentContext, + } = value ?? {}; const clusters = newClusters(rawClusters); const users = newUsers(rawUsers); const contexts = newContexts(rawContexts); @@ -175,66 +171,78 @@ export interface SplitConfigEntry { * Breaks kube config into several configs. Each context as it own KubeConfig object */ export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] { - const { contexts = [] } = kubeConfig; - - return contexts.map(context => { + return kubeConfig.getContexts().map(ctx => { const config = new KubeConfig(); + const cluster = kubeConfig.getCluster(ctx.cluster); + const user = kubeConfig.getUser(ctx.user); + const context = kubeConfig.getContextObject(ctx.name); - config.clusters = [kubeConfig.getCluster(context.cluster)].filter(Boolean); - config.users = [kubeConfig.getUser(context.user)].filter(Boolean); - config.contexts = [kubeConfig.getContextObject(context.name)].filter(Boolean); - config.setCurrentContext(context.name); + if (cluster) { + config.addCluster(cluster); + } + + if (user) { + config.addUser(user); + } + + if (context) { + config.addContext(context); + } + + config.setCurrentContext(ctx.name); return { config, - error: validateKubeConfig(config, context.name)?.toString(), + error: validateKubeConfig(config, ctx.name)?.toString(), }; }); } +/** + * Pretty format the object as human readable yaml, such as would be on the filesystem + * @param kubeConfig The kubeconfig object to format as pretty yaml + * @returns The yaml representation of the kubeconfig object + */ export function dumpConfigYaml(kubeConfig: Partial): string { + const clusters = kubeConfig.clusters.map(cluster => ({ + name: cluster.name, + cluster: { + "certificate-authority-data": cluster.caData, + "certificate-authority": cluster.caFile, + server: cluster.server, + "insecure-skip-tls-verify": cluster.skipTLSVerify + } + })); + const contexts = kubeConfig.contexts.map(context => ({ + name: context.name, + context: { + cluster: context.cluster, + user: context.user, + namespace: context.namespace + } + })); + const users = kubeConfig.users.map(user => ({ + name: user.name, + user: { + "client-certificate-data": user.certData, + "client-certificate": user.certFile, + "client-key-data": user.keyData, + "client-key": user.keyFile, + "auth-provider": user.authProvider, + exec: user.exec, + token: user.token, + username: user.username, + password: user.password + } + })); const config = { apiVersion: "v1", kind: "Config", preferences: {}, "current-context": kubeConfig.currentContext, - clusters: kubeConfig.clusters.map(cluster => { - return { - name: cluster.name, - cluster: { - "certificate-authority-data": cluster.caData, - "certificate-authority": cluster.caFile, - server: cluster.server, - "insecure-skip-tls-verify": cluster.skipTLSVerify - } - }; - }), - contexts: kubeConfig.contexts.map(context => { - return { - name: context.name, - context: { - cluster: context.cluster, - user: context.user, - namespace: context.namespace - } - }; - }), - users: kubeConfig.users.map(user => { - return { - name: user.name, - user: { - "client-certificate-data": user.certData, - "client-certificate": user.certFile, - "client-key-data": user.keyData, - "client-key": user.keyFile, - "auth-provider": user.authProvider, - exec: user.exec, - token: user.token, - username: user.username, - password: user.password - } - }; - }) + clusters, + contexts, + users, }; logger.debug("Dumping KubeConfig:", config); @@ -243,70 +251,25 @@ export function dumpConfigYaml(kubeConfig: Partial): string { return yaml.safeDump(config, { skipInvalid: true }); } -export function podHasIssues(pod: V1Pod) { - // Logic adapted from dashboard - const notReady = !!pod.status.conditions.find(condition => { - return condition.type == "Ready" && condition.status !== "True"; - }); - - return ( - notReady || - pod.status.phase !== "Running" || - pod.spec.priority > 500000 // We're interested in high priority pods events regardless of their running status - ); -} - -export function getNodeWarningConditions(node: V1Node) { - return node.status?.conditions?.filter(c => - c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" - ) ?? []; -} - /** * Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required) * * Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes */ -export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined { - try { - // 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 +export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined { + const contextObject = config.getContextObject(contextName); - const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts; - - const contextObject = config.getContextObject(contextName); - - // Validate the Context Object - if (!contextObject) { - return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`); - } - - // Validate the Cluster Object - if (validateCluster && !config.getCluster(contextObject.cluster)) { - return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`); - } - - const user = config.getUser(contextObject.user); - - // Validate the User Object - if (validateUser && !user) { - return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`); - } - - // Validate exec command if present - if (validateExec && 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 - if (!commandExists.sync(execCommand)) { - return new ExecValidationNotFoundError(execCommand, isAbsolute); - } - } - - return undefined; - } catch (error) { - return error; + if (!contextObject) { + return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`); } + + if (!config.getCluster(contextObject.cluster)) { + return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`); + } + + if (!config.getUser(contextObject.user)) { + return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`); + } + + return undefined; } diff --git a/types/command-exists.d.ts b/types/command-exists.d.ts deleted file mode 100644 index 8f07bb978e..0000000000 --- a/types/command-exists.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -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; -} diff --git a/yarn.lock b/yarn.lock index bd4f062a41..006af08175 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4066,11 +4066,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, 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@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"