1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Allow user to configure kubectl binary preferences (#800)

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Lauri Nevala 2020-09-04 22:50:50 +03:00 committed by GitHub
parent 8f94f4a3da
commit 4250523fe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 88 deletions

View File

@ -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<UserStoreModel> {
@ -53,6 +58,9 @@ export class UserStore extends BaseStore<UserStoreModel> {
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<UserStoreModel> {
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<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data

View File

@ -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<string> {
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<boolean> {
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() {

View File

@ -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<string, any> = 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<Array<string>> {
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:

View File

@ -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<string>[] = [
{ 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 (
<>
<SubTitle title="Path to Kubectl binary"/>
<Input
theme="round-black"
value={binariesPath}
validators={isPath}
onChange={setBinariesPath}
onBlur={save}
/>
<small className="hint">
<Trans>Default:</Trans>{" "}{Kubectl.bundledKubectlPath}
</small>
</>
);
}
return (
<>
<h2><Trans>Kubectl Binary</Trans></h2>
<small className="hint">
<Trans>Download kubectl binaries matching to Kubernetes cluster verison.</Trans>
</small>
<Checkbox
label={<Trans>Download kubectl binaries</Trans>}
value={preferences.downloadKubectlBinaries}
onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries}
/>
<SubTitle title="Download mirror" />
<Select
placeholder={<Trans>Download mirror for kubectl</Trans>}
options={downloadMirrorOptions}
value={preferences.downloadMirror}
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
disabled={!preferences.downloadKubectlBinaries}
/>
<SubTitle title="Directory for binaries"/>
<Input
theme="round-black"
value={downloadPath}
placeholder={`Directory to download binaries into`}
validators={isPath}
onChange={setDownloadPath}
onBlur={save}
disabled={!preferences.downloadKubectlBinaries}
/>
<small>
Default: {userStore.getDefaultKubectlPath()}
</small>
{renderPath()}
</>
);
});

View File

@ -19,6 +19,11 @@
}
}
.SubTitle {
text-transform: none;
margin: 0!important;
}
.repos {
position: relative;

View File

@ -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<string, HelmRepo>();
@observable downloadMirrorOptions: SelectOption<string>[] = [
{ value: "default", label: "Default (Google)" },
{ value: "china", label: "China (Azure)" },
]
@observable httpProxy = userStore.preferences.httpsProxy || "";
@computed get themeOptions(): SelectOption<string>[] {
@ -135,13 +130,19 @@ export class Preferences extends React.Component {
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
/>
<h2><Trans>Download Mirror</Trans></h2>
<Select
placeholder={<Trans>Download mirror for kubectl</Trans>}
options={this.downloadMirrorOptions}
value={preferences.downloadMirror}
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
<h2><Trans>HTTP Proxy</Trans></h2>
<Input
theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={this.httpProxy}
onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
/>
<small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans>
</small>
<KubectlBinaries preferences={preferences} />
<h2><Trans>Helm</Trans></h2>
<Select
@ -172,18 +173,6 @@ export class Preferences extends React.Component {
})}
</div>
<h2><Trans>HTTP Proxy</Trans></h2>
<Input
theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={this.httpProxy}
onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
/>
<small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans>
</small>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}

View File

@ -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}`),