diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 6f3e2605be..a0cfe09fd5 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -1,4 +1,5 @@ import type { ThemeId } from "../renderer/theme.store"; +import { app, remote } from 'electron'; import semver from "semver" import { readFile } from "fs-extra" import { action, observable, reaction, toJS } from "mobx"; @@ -8,6 +9,7 @@ import { getAppVersion } from "./utils/app-version"; import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { tracker } from "./tracker"; import logger from "../main/logger"; +import path from 'path'; export interface UserStoreModel { kubeConfigPath: string; @@ -22,6 +24,9 @@ export interface UserPreferences { allowUntrustedCAs?: boolean; allowTelemetry?: boolean; downloadMirror?: string | "default"; + downloadKubectlBinaries?: boolean; + downloadBinariesPath?: string; + kubectlBinariesPath?: string; } export class UserStore extends BaseStore { @@ -53,6 +58,9 @@ export class UserStore extends BaseStore { allowUntrustedCAs: false, colorTheme: UserStore.defaultTheme, downloadMirror: "default", + downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version + downloadBinariesPath: this.getDefaultKubectlPath(), + kubectlBinariesPath: "" }; get isNewVersion() { @@ -98,6 +106,14 @@ export class UserStore extends BaseStore { this.newContexts.clear(); } + /** + * Getting default directory to download kubectl binaries + * @returns string + */ + getDefaultKubectlPath(): string { + return path.join((app || remote.app).getPath("userData"), "binaries") + } + @action protected async fromStore(data: Partial = {}) { const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 8973d6e3b8..afb7a89111 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -97,7 +97,7 @@ export class Kubectl { this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}` - this.dirname = path.normalize(path.join(Kubectl.kubectlDir, this.kubectlVersion)) + this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion)) this.path = path.join(this.dirname, binaryName) } @@ -105,7 +105,19 @@ export class Kubectl { return Kubectl.bundledKubectlPath } + public getPathFromPreferences() { + return userStore.preferences?.kubectlBinariesPath || this.getBundledPath() + } + + protected getDownloadDir() { + return userStore.preferences?.downloadBinariesPath || Kubectl.kubectlDir + } + public async getPath(bundled = false): Promise { + if (userStore.preferences?.downloadKubectlBinaries === false) { + return this.getPathFromPreferences() + } + // return binary name if bundled path is not functional if (!await this.checkBinary(this.getBundledPath(), false)) { Kubectl.invalidBundle = true @@ -128,6 +140,7 @@ export class Kubectl { public async binDir() { try { await this.ensureKubectl() + await this.writeInitScripts() return this.dirname } catch (err) { logger.error(err) @@ -180,6 +193,9 @@ export class Kubectl { } public async ensureKubectl(): Promise { + if (userStore.preferences?.downloadKubectlBinaries === false) { + return true + } if (Kubectl.invalidBundle) { logger.error(`Detected invalid bundle binary, returning ...`) return false @@ -203,10 +219,6 @@ export class Kubectl { release() return false } - await this.writeInitScripts().catch((error) => { - logger.error("Failed to write init scripts"); - logger.error(error) - }) logger.debug(`Releasing lock for ${this.kubectlVersion}`) release() return true @@ -249,71 +261,52 @@ export class Kubectl { }) } - protected async scriptIsLatest(scriptPath: string) { - const scriptExists = await pathExists(scriptPath) - if (!scriptExists) return false - - try { - const filehandle = await fs.promises.open(scriptPath, 'r') - const buffer = Buffer.alloc(40) - await filehandle.read(buffer, 0, 40, 0) - await filehandle.close() - return buffer.toString().startsWith(initScriptVersionString) - } catch (err) { - logger.error(err) - return false - } - } - protected async writeInitScripts() { + const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()) const helmPath = helmCli.getBinaryDir() const fsPromises = fs.promises; const bashScriptPath = path.join(this.dirname, '.bash_set_path') - const bashScriptIsLatest = await this.scriptIsLatest(bashScriptPath) - if (!bashScriptIsLatest) { - let bashScript = "" + initScriptVersionString - bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n" - bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n" - bashScript += "if test -f \"$HOME/.bash_profile\"; then\n" - bashScript += " . \"$HOME/.bash_profile\"\n" - bashScript += "elif test -f \"$HOME/.bash_login\"; then\n" - bashScript += " . \"$HOME/.bash_login\"\n" - bashScript += "elif test -f \"$HOME/.profile\"; then\n" - bashScript += " . \"$HOME/.profile\"\n" - bashScript += "fi\n" - bashScript += `export PATH="${this.dirname}:${helmPath}:$PATH"\n` - bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" - bashScript += "unset tempkubeconfig\n" - await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }) - } + + let bashScript = "" + initScriptVersionString + bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n" + bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n" + bashScript += "if test -f \"$HOME/.bash_profile\"; then\n" + bashScript += " . \"$HOME/.bash_profile\"\n" + bashScript += "elif test -f \"$HOME/.bash_login\"; then\n" + bashScript += " . \"$HOME/.bash_login\"\n" + bashScript += "elif test -f \"$HOME/.profile\"; then\n" + bashScript += " . \"$HOME/.profile\"\n" + bashScript += "fi\n" + bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n` + bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" + bashScript += "unset tempkubeconfig\n" + await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }) const zshScriptPath = path.join(this.dirname, '.zlogin') - const zshScriptIsLatest = await this.scriptIsLatest(zshScriptPath) - if (!zshScriptIsLatest) { - let zshScript = "" + initScriptVersionString - zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n" - // restore previous ZDOTDIR - zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n" - // source all the files - zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n" + let zshScript = "" + initScriptVersionString - // voodoo to replace any previous occurrences of kubectl path in the PATH - zshScript += `kubectlpath=\"${this.dirname}"\n` - zshScript += `helmpath=\"${helmPath}"\n` - zshScript += "p=\":$kubectlpath:\"\n" - zshScript += "d=\":$PATH:\"\n" - zshScript += "d=${d//$p/:}\n" - zshScript += "d=${d/#:/}\n" - zshScript += "export PATH=\"$kubectlpath:$helmpath:${d/%:/}\"\n" - zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" - zshScript += "unset tempkubeconfig\n" - zshScript += "unset OLD_ZDOTDIR\n" - await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 }) - } + zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n" + // restore previous ZDOTDIR + zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n" + // source all the files + zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n" + zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n" + zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n" + zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n" + + // voodoo to replace any previous occurrences of kubectl path in the PATH + zshScript += `kubectlpath=\"${kubectlPath}"\n` + zshScript += `helmpath=\"${helmPath}"\n` + zshScript += "p=\":$kubectlpath:\"\n" + zshScript += "d=\":$PATH:\"\n" + zshScript += "d=${d//$p/:}\n" + zshScript += "d=${d/#:/}\n" + zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n" + zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" + zshScript += "unset tempkubeconfig\n" + zshScript += "unset OLD_ZDOTDIR\n" + await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 }) } protected getDownloadMirror() { diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index b02b038aab..704d97382a 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -10,6 +10,7 @@ import { ClusterPreferences } from "../common/cluster-store"; import { helmCli } from "./helm/helm-cli" import { isWindows } from "../common/vars"; import { tracker } from "../common/tracker"; +import { userStore } from "../common/user-store"; export class ShellSession extends EventEmitter { static shellEnvs: Map = new Map() @@ -20,6 +21,7 @@ export class ShellSession extends EventEmitter { protected nodeShellPod: string; protected kubectl: Kubectl; protected kubectlBinDir: string; + protected kubectlPathDir: string; protected helmBinDir: string; protected preferences: ClusterPreferences; protected running = false; @@ -36,6 +38,8 @@ export class ShellSession extends EventEmitter { public async open() { this.kubectlBinDir = await this.kubectl.binDir() + const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath + this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectl.binDir() : path.dirname(pathFromPreferences) this.helmBinDir = helmCli.getBinaryDir() const env = await this.getCachedShellEnv() const shell = env.PTYSHELL @@ -67,11 +71,11 @@ export class ShellSession extends EventEmitter { protected async getShellArgs(shell: string): Promise> { switch(path.basename(shell)) { case "powershell.exe": - return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.kubectlBinDir};${this.helmBinDir};$Env:PATH"}`] + return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`] case "bash": return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')] case "fish": - return ["--login", "--init-command", `export PATH="${this.kubectlBinDir}:${this.helmBinDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`] + return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`] case "zsh": return ["--login"] default: diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx new file mode 100644 index 0000000000..15b01251f4 --- /dev/null +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { Trans } from '@lingui/macro'; +import { isPath } from '../input/input.validators'; +import { Checkbox } from '../checkbox'; +import { Input } from '../input'; +import { SubTitle } from '../layout/sub-title'; +import { UserPreferences, userStore } from '../../../common/user-store'; +import { observer } from 'mobx-react'; +import { Kubectl } from '../../../main/kubectl'; +import { SelectOption, Select } from '../select'; + +export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { + const [downloadPath, setDownloadPath] = useState(preferences.downloadBinariesPath || ""); + const [binariesPath, setBinariesPath] = useState(preferences.kubectlBinariesPath || ""); + + const downloadMirrorOptions: SelectOption[] = [ + { value: "default", label: "Default (Google)" }, + { value: "china", label: "China (Azure)" }, + ] + + + const save = () => { + preferences.downloadBinariesPath = downloadPath; + preferences.kubectlBinariesPath = binariesPath; + } + + const renderPath = () => { + if (preferences.downloadKubectlBinaries) { + return null; + } + return ( + <> + + + + Default:{" "}{Kubectl.bundledKubectlPath} + + + ); + } + + return ( + <> +

Kubectl Binary

+ + Download kubectl binaries matching to Kubernetes cluster verison. + + Download kubectl binaries} + value={preferences.downloadKubectlBinaries} + onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries} + /> + + + + Default: {userStore.getDefaultKubectlPath()} + + {renderPath()} + + ); +}); \ No newline at end of file diff --git a/src/renderer/components/+preferences/preferences.scss b/src/renderer/components/+preferences/preferences.scss index 33072a4849..62bd307cd1 100644 --- a/src/renderer/components/+preferences/preferences.scss +++ b/src/renderer/components/+preferences/preferences.scss @@ -19,6 +19,11 @@ } } + .SubTitle { + text-transform: none; + margin: 0!important; + } + .repos { position: relative; diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index cb9dcbc853..a1b86c19cd 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -16,18 +16,13 @@ import { Badge } from "../badge"; import { themeStore } from "../../theme.store"; import { history } from "../../navigation"; import { Tooltip } from "../tooltip"; +import { KubectlBinaries } from "./kubectl-binaries"; @observer export class Preferences extends React.Component { @observable helmLoading = false; @observable helmRepos: HelmRepo[] = []; @observable helmAddedRepos = observable.map(); - - @observable downloadMirrorOptions: SelectOption[] = [ - { value: "default", label: "Default (Google)" }, - { value: "china", label: "China (Azure)" }, - ] - @observable httpProxy = userStore.preferences.httpsProxy || ""; @computed get themeOptions(): SelectOption[] { @@ -135,13 +130,19 @@ export class Preferences extends React.Component { onChange={({ value }: SelectOption) => preferences.colorTheme = value} /> -

Download Mirror

- this.httpProxy = v} + onBlur={() => preferences.httpsProxy = this.httpProxy} /> + + Proxy is used only for non-cluster communication. + + +

Helm

this.httpProxy = v} - onBlur={() => preferences.httpsProxy = this.httpProxy} - /> - - Proxy is used only for non-cluster communication. - -

Certificate Trust

Allow untrusted Certificate Authorities} diff --git a/src/renderer/components/input/input.validators.ts b/src/renderer/components/input/input.validators.ts index 2236265e5d..4e18468da9 100644 --- a/src/renderer/components/input/input.validators.ts +++ b/src/renderer/components/input/input.validators.ts @@ -2,6 +2,7 @@ import type { InputProps } from "./input"; import { ReactNode } from "react"; import { t } from "@lingui/macro"; import { _i18n } from '../../i18n'; +import fse from "fs-extra"; export interface Validator { debounce?: number; // debounce for async validators in ms @@ -41,6 +42,12 @@ export const isUrl: Validator = { validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), }; +export const isPath: Validator = { + condition: ({ type }) => type === "text", + message: () => _i18n._(t`This field must be a path to an existing file`), + validate: value => !value || fse.pathExistsSync(value), +} + export const minLength: Validator = { condition: ({ minLength }) => !!minLength, message: (value, { minLength }) => _i18n._(t`Minimum length is ${minLength}`),