Skip to content

Commit

Permalink
Allow user to configure kubectl binary preferences (#800)
Browse files Browse the repository at this point in the history
Signed-off-by: Lauri Nevala <[email protected]>

Co-authored-by: Alex Andreev <[email protected]>
  • Loading branch information
nevalla and aleksfront authored Sep 4, 2020
1 parent 8f94f4a commit 4250523
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 90 deletions.
16 changes: 16 additions & 0 deletions src/common/user-store.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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> {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
121 changes: 57 additions & 64 deletions src/main/kubectl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,27 @@ 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)
}

public getBundledPath() {
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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"

// 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 })
}

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"

// 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() {
Expand Down
8 changes: 6 additions & 2 deletions src/main/shell-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
83 changes: 83 additions & 0 deletions src/renderer/components/+preferences/kubectl-binaries.tsx
Original file line number Diff line number Diff line change
@@ -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()}
</>
);
});
5 changes: 5 additions & 0 deletions src/renderer/components/+preferences/preferences.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
}
}

.SubTitle {
text-transform: none;
margin: 0!important;
}

.repos {
position: relative;

Expand Down
Loading

0 comments on commit 4250523

Please sign in to comment.