Consolidate downloading binaries into one script (#4942)

* Consolidate downloading binaries into one script

- Upgrade lens-k8s-proxy to 0.1.5 which allows us to remove the trailing
  slash in KubeAuthProxy

- Consolidate nromalizing arch and os strings

- Remove LensBinary as HelmCli is not just a raw object since it is
  always bundled

- Introduce OnceCell and use it for the binary paths

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully remove helmCli, move associated variable to vars

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix helm downloading on windows

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Don't download binaries for unsupported windows

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Arch specific paths

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix downloading helm on windows

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* rename once-cell file to lazy-initialized

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-03-09 17:51:51 -05:00
1 parent dd5dfb393d
commit a19f094a0d
23 files changed
+472 -641

No files matched your search

@@ -3,19 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import isDevelopmentInjectable from "../../vars/is-development.injectable";
import contextDirInjectable from "../../vars/context-dir.injectable";
import { baseBinariesDir } from "../../vars";
const directoryForBundledBinariesInjectable = getInjectable({
id: "directory-for-bundled-binaries",
instantiate: (di) => {
if (di.inject(isDevelopmentInjectable)) {
return path.join(di.inject(contextDirInjectable), "binaries");
}
return process.resourcesPath;
},
instantiate: () => baseBinariesDir.get(),
lifecycle: lifecycleEnum.singleton,
});
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A OnceCell is an object that wraps some function that produces a value.
*
* It then only calls the function on the first call to `get()` and returns the
* same instance/value on every subsequent call.
*/
export interface LazyInitialized<T> {
get(): T;
}
/**
* A function to make a `OnceCell<T>`
*/
export function lazyInitialized<T>(builder: () => T): LazyInitialized<T> {
let value: T | undefined;
let called = false;
return {
get() {
if (called) {
return value;
}
value = builder();
called = true;
return value;
},
};
}
+49
View File
@@ -8,6 +8,7 @@ import path from "path";
import { SemVer } from "semver";
import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal";
import { lazyInitialized } from "./utils/lazy-initialized";
export const isMac = process.platform === "darwin";
export const isWindows = process.platform === "win32";
@@ -29,6 +30,54 @@ export const defaultTheme = "lens-dark" as string;
export const defaultFontSize = 12;
export const defaultTerminalFontFamily = "RobotoMono";
export const defaultEditorFontFamily = "RobotoMono";
export const normalizedPlatform = (() => {
switch (process.platform) {
case "darwin":
return "darwin";
case "linux":
return "linux";
case "win32":
return "windows";
default:
throw new Error(`platform=${process.platform} is unsupported`);
}
})();
export const normalizedArch = (() => {
switch (process.arch) {
case "arm64":
return "arm64";
case "x64":
case "amd64":
return "x64";
case "386":
case "x32":
case "ia32":
return "ia32";
default:
throw new Error(`arch=${process.arch} is unsupported`);
}
})();
export function getBinaryName(name: string, { forPlatform = normalizedPlatform } = {}): string {
if (forPlatform === "windows") {
return `${name}.exe`;
}
return name;
}
const resourcesDir = lazyInitialized(() => (
isProduction
? process.resourcesPath
: path.join(process.cwd(), "binaries", "client", normalizedPlatform)
));
export const baseBinariesDir = lazyInitialized(() => path.join(resourcesDir.get(), normalizedArch));
export const kubeAuthProxyBinaryName = getBinaryName("lens-k8s-proxy");
export const helmBinaryName = getBinaryName("helm");
export const helmBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), helmBinaryName));
export const kubectlBinaryName = getBinaryName("kubectl");
export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName));
// Webpack build paths
export const contextDir = process.cwd();
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { promiseExecFile } from "../../common/utils/promise-exec";
import type { BaseEncodingOptions } from "fs";
import type { ExecFileOptions } from "child_process";
import { helmBinaryPath } from "../../common/vars";
/**
* ExecFile the bundled helm CLI
* @returns STDOUT
*/
export async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
try {
const { stdout } = await promiseExecFile(helmBinaryPath.get(), args, options);
return stdout;
} catch (error) {
throw error?.stderr || error;
}
}
+2 -11
View File
@@ -8,10 +8,9 @@ import v8 from "v8";
import * as yaml from "js-yaml";
import type { HelmRepo } from "./helm-repo-manager";
import logger from "../logger";
import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { iter, sortCharts } from "../../common/utils";
import { execHelm } from "./exec";
interface ChartCacheEntry {
data: Buffer;
@@ -49,21 +48,13 @@ export class HelmChartManager {
}
private async executeCommand(args: string[], name: string, version?: string) {
const helm = await helmCli.binaryPath();
args.push(`${this.repo.name}/${name}`);
if (version) {
args.push("--version", version);
}
try {
const { stdout } = await promiseExecFile(helm, args);
return stdout;
} catch (error) {
throw error.stderr || error;
}
return execHelm(args);
}
public async getReadme(name: string, version?: string) {
-49
View File
@@ -1,49 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import packageInfo from "../../../package.json";
import path from "path";
import { LensBinary, LensBinaryOpts } from "../lens-binary";
import { isProduction } from "../../common/vars";
export class HelmCli extends LensBinary {
public constructor(baseDir: string, version: string) {
const opts: LensBinaryOpts = {
version,
baseDir,
originalBinaryName: "helm",
newBinaryName: "helm3",
};
super(opts);
}
protected getTarName(): string | null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getUrl() {
return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getBinaryPath() {
return path.join(this.dirname, this.binaryName);
}
protected getOriginalBinaryPath() {
return path.join(this.dirname, `${this.platformName}-${this.arch}`, this.originalBinaryName);
}
}
const helmVersion = packageInfo.config.bundledHelmVersion;
let baseDir = process.resourcesPath;
if (!isProduction) {
baseDir = path.join(process.cwd(), "binaries", "client", process.arch);
}
export const helmCli = new HelmCli(baseDir, helmVersion);
+2 -16
View File
@@ -6,23 +6,9 @@
import tempy from "tempy";
import fse from "fs-extra";
import * as yaml from "js-yaml";
import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli";
import { toCamelCase } from "../../common/utils/camelCase";
import type { BaseEncodingOptions } from "fs";
import { execFile, ExecFileOptions } from "child_process";
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
const helmCliPath = await helmCli.binaryPath();
try {
const { stdout } = await promiseExecFile(helmCliPath, args, options);
return stdout;
} catch (error) {
throw error?.stderr || error;
}
}
import { execFile } from "child_process";
import { execHelm } from "./exec";
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
const args = [
+2 -19
View File
@@ -4,14 +4,12 @@
*/
import yaml from "js-yaml";
import { BaseEncodingOptions, readFile } from "fs-extra";
import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli";
import { readFile } from "fs-extra";
import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import type { ExecFileOptions } from "child_process";
import { execHelm } from "./exec";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
@@ -34,18 +32,6 @@ export interface HelmRepo {
password?: string;
}
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
const helmCliPath = await helmCli.binaryPath();
try {
const { stdout } = await promiseExecFile(helmCliPath, args, options);
return stdout;
} catch (error) {
throw error?.stderr || error;
}
}
export class HelmRepoManager extends Singleton {
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
@@ -63,9 +49,6 @@ export class HelmRepoManager extends Singleton {
}
private async ensureInitialized() {
helmCli.setLogger(logger);
await helmCli.ensureBinary();
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list();
@@ -3,24 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { KubeAuthProxy } from "./kube-auth-proxy";
import { KubeAuthProxy, KubeAuthProxyDependencies } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster";
import path from "path";
import { isDevelopment, isWindows } from "../../common/vars";
import { getBinaryName } from "../../common/vars";
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
const createKubeAuthProxyInjectable = getInjectable({
id: "create-kube-auth-proxy",
instantiate: (di) => {
const binaryName = isWindows ? "lens-k8s-proxy.exe" : "lens-k8s-proxy";
const proxyPath = isDevelopment ? path.join("client", process.platform, process.arch) : process.arch;
const dependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), proxyPath, binaryName),
const binaryName = getBinaryName("lens-k8s-proxy");
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
};
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) =>
new KubeAuthProxy(dependencies, cluster, environmentVariables);
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => (
new KubeAuthProxy(dependencies, cluster, environmentVariables)
);
},
});
+4 -4
View File
@@ -13,7 +13,7 @@ import { makeObservable, observable, when } from "mobx";
const startingServeRegex = /starting to serve on (?<address>.+)/i;
interface Dependencies {
export interface KubeAuthProxyDependencies {
proxyBinPath: string;
}
@@ -28,7 +28,7 @@ export class KubeAuthProxy {
protected proxyProcess?: ChildProcess;
@observable protected ready = false;
constructor(private dependencies: Dependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
constructor(private dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
makeObservable(this);
}
@@ -42,8 +42,8 @@ export class KubeAuthProxy {
}
const proxyBin = this.dependencies.proxyBinPath;
this.proxyProcess = spawn(proxyBin, [], {
this.proxyProcess = spawn(proxyBin, [], {
env: {
...this.env,
KUBECONFIG: this.cluster.kubeConfigPath,
+8 -42
View File
@@ -9,9 +9,8 @@ import { promiseExecFile } from "../../common/utils/promise-exec";
import logger from "../logger";
import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile";
import { helmCli } from "../helm/helm-cli";
import { getBundledKubectlVersion } from "../../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../../common/vars";
import { normalizedPlatform, normalizedArch, kubectlBinaryName, kubectlBinaryPath, baseBinariesDir } from "../../common/vars";
import { SemVer } from "semver";
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
import got from "got/dist/source";
@@ -39,27 +38,8 @@ const kubectlMap: Map<string, string> = new Map([
["1.22", "1.22.6"],
["1.23", bundledVersion],
]);
let bundledPath: string;
const initScriptVersionString = "# lens-initscript v3";
export function bundledKubectlPath(): string {
if (bundledPath) { return bundledPath; }
if (isDevelopment || isTestEnv) {
const platformName = isWindows ? "windows" : process.platform;
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
} else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
}
if (isWindows) {
bundledPath = `${bundledPath}.exe`;
}
return bundledPath;
}
interface Dependencies {
directoryForKubectlBinaries: string;
@@ -102,27 +82,13 @@ export class Kubectl {
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
}
let arch = null;
if (process.arch == "x64") {
arch = "amd64";
} else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386";
} else {
arch = process.arch;
}
const platformName = isWindows ? "windows" : process.platform;
const binaryName = isWindows ? "kubectl.exe" : "kubectl";
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`;
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${normalizedPlatform}/${normalizedArch}/${kubectlBinaryName}`;
this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion));
this.path = path.join(this.dirname, binaryName);
this.path = path.join(this.dirname, kubectlBinaryName);
}
public getBundledPath() {
return bundledKubectlPath();
return kubectlBinaryPath.get();
}
public getPathFromPreferences() {
@@ -316,7 +282,7 @@ export class Kubectl {
? this.dirname
: path.dirname(this.getPathFromPreferences());
const helmPath = helmCli.getBinaryDir();
const binariesDir = baseBinariesDir.get();
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
const bashScript = [
@@ -330,7 +296,7 @@ export class Kubectl {
"elif test -f \"$HOME/.profile\"; then",
" . \"$HOME/.profile\"",
"fi",
`export PATH="${helmPath}:${kubectlPath}:$PATH"`,
`export PATH="${binariesDir}:${kubectlPath}:$PATH"`,
'export KUBECONFIG="$tempkubeconfig"',
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
@@ -356,12 +322,12 @@ export class Kubectl {
// voodoo to replace any previous occurrences of kubectl path in the PATH
`kubectlpath="${kubectlPath}"`,
`helmpath="${helmPath}"`,
`binariesDir="${binariesDir}"`,
"p=\":$kubectlpath:\"",
"d=\":$PATH:\"",
`d=\${d//$p/:}`,
`d=\${d/#:/}`,
`export PATH="$helmpath:$kubectlpath:\${d/%:/}"`,
`export PATH="$binariesDir:$kubectlpath:\${d/%:/}"`,
"export KUBECONFIG=\"$tempkubeconfig\"",
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
-202
View File
@@ -1,202 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import fs from "fs";
import request from "request";
import { ensureDir, pathExists } from "fs-extra";
import * as tar from "tar";
import { isWindows } from "../common/vars";
import type winston from "winston";
export interface LensBinaryOpts {
version: string;
baseDir: string;
originalBinaryName: string;
newBinaryName?: string;
requestOpts?: request.Options;
}
export class LensBinary {
public binaryVersion: string;
protected directory: string;
protected url: string;
protected path: string;
protected tarPath: string;
protected dirname: string;
protected binaryName: string;
protected platformName: string;
protected arch: string;
protected originalBinaryName: string;
protected requestOpts: request.Options;
protected logger: Console | winston.Logger;
constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir;
this.originalBinaryName = opts.originalBinaryName;
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
this.binaryVersion = opts.version;
this.requestOpts = opts.requestOpts;
this.logger = console;
let arch = null;
if (process.env.BINARY_ARCH) {
arch = process.env.BINARY_ARCH;
} else if (process.arch == "x64") {
arch = "amd64";
} else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386";
} else {
arch = process.arch;
}
this.arch = arch;
this.platformName = isWindows ? "windows" : process.platform;
this.dirname = path.normalize(path.join(baseDir, this.binaryName));
if (isWindows) {
this.binaryName = `${this.binaryName}.exe`;
this.originalBinaryName = `${this.originalBinaryName}.exe`;
}
const tarName = this.getTarName();
if (tarName) {
this.tarPath = path.join(this.dirname, tarName);
}
}
public setLogger(logger: Console | winston.Logger) {
this.logger = logger;
}
protected binaryDir() {
throw new Error("binaryDir not implemented");
}
public async binaryPath() {
await this.ensureBinary();
return this.getBinaryPath();
}
protected getTarName(): string | null {
return null;
}
protected getUrl() {
return "";
}
protected getBinaryPath() {
return "";
}
protected getOriginalBinaryPath() {
return "";
}
public getBinaryDir() {
return path.dirname(this.getBinaryPath());
}
public async binDir() {
try {
await this.ensureBinary();
return this.dirname;
} catch (err) {
this.logger.error(err);
return "";
}
}
protected async checkBinary() {
const exists = await pathExists(this.getBinaryPath());
return exists;
}
public async ensureBinary() {
const isValid = await this.checkBinary();
if (!isValid) {
await this.downloadBinary().catch((error) => {
this.logger.error(error);
});
if (this.tarPath) await this.untarBinary();
if (this.originalBinaryName != this.binaryName) await this.renameBinary();
this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`);
}
}
protected async untarBinary() {
return new Promise<void>(resolve => {
this.logger.debug(`Extracting ${this.originalBinaryName} binary`);
tar.x({
file: this.tarPath,
cwd: this.dirname,
}).then((() => {
resolve();
}));
});
}
protected async renameBinary() {
return new Promise<void>((resolve, reject) => {
this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`);
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath();
await ensureDir(this.getBinaryDir(), 0o755);
const file = fs.createWriteStream(binaryPath);
const url = this.getUrl();
this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`);
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: url,
gzip: true,
...this.requestOpts,
};
const stream = request(requestOpts);
stream.on("complete", () => {
this.logger.info(`Download of ${this.originalBinaryName} finished`);
file.end();
});
stream.on("error", (error) => {
this.logger.error(error);
fs.unlink(binaryPath, () => {
// do nothing
});
throw(error);
});
return new Promise<void>((resolve, reject) => {
file.on("close", () => {
this.logger.debug(`${this.originalBinaryName} binary download closed`);
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
if (err) reject(err);
});
resolve();
});
stream.pipe(file);
});
}
}
@@ -5,12 +5,12 @@
import type WebSocket from "ws";
import path from "path";
import { helmCli } from "../../helm/helm-cli";
import { UserStore } from "../../../common/user-store";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import { ShellSession } from "../shell-session";
import type { Kubectl } from "../../kubectl/kubectl";
import { baseBinariesDir } from "../../../common/vars";
export class LocalShellSession extends ShellSession {
ShellType = "shell";
@@ -18,9 +18,9 @@ export class LocalShellSession extends ShellSession {
constructor(protected shellEnvModify: (clusterId: ClusterId, env: Record<string, string>) => Record<string, string>, kubectl: Kubectl, websocket: WebSocket, cluster: Cluster, terminalId: string) {
super(kubectl, websocket, cluster, terminalId);
}
protected getPathEntries(): string[] {
return [helmCli.getBinaryDir()];
return [baseBinariesDir.get()];
}
protected get cwd(): string | undefined {
@@ -40,17 +40,16 @@ export class LocalShellSession extends ShellSession {
}
protected async getShellArgs(shell: string): Promise<string[]> {
const helmpath = helmCli.getBinaryDir();
const pathFromPreferences = UserStore.getInstance().kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = UserStore.getInstance().downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
switch(path.basename(shell)) {
case "powershell.exe":
return ["-NoExit", "-command", `& {$Env:PATH="${helmpath};${kubectlPathDir};$Env:PATH"}`];
return ["-NoExit", "-command", `& {$Env:PATH="${baseBinariesDir.get()};${kubectlPathDir};$Env:PATH"}`];
case "bash":
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
case "fish":
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
return ["--login", "--init-command", `export PATH="${baseBinariesDir.get()}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
case "zsh":
return ["--login"];
default:
@@ -8,13 +8,12 @@ import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { UserStore } from "../../../common/user-store";
import { bundledKubectlPath } from "../../../main/kubectl/kubectl";
import { SelectOption, Select } from "../select";
import { Switch } from "../switch";
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
import directoryForBinariesInjectable
from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import { kubectlBinaryPath } from "../../../common/vars";
interface Dependencies {
defaultPathForKubectlBinaries: string;
@@ -80,7 +79,7 @@ const NonInjectedKubectlBinaries: React.FC<Dependencies> = observer(({ defaultPa
<SubTitle title="Path to kubectl binary" />
<Input
theme="round-black"
placeholder={bundledKubectlPath()}
placeholder={kubectlBinaryPath.get()}
value={binariesPath}
validators={pathValidator}
onChange={setBinariesPath}