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

Add option to disable kubectl downloading

- Defaults to false
- falls back to bundled kubectl version

Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton 2020-08-04 08:57:26 -04:00
parent 1cf446ea98
commit f6d9a05002
7 changed files with 234 additions and 191 deletions

View File

@ -19,6 +19,7 @@ export interface UserPreferences {
allowUntrustedCAs?: boolean; allowUntrustedCAs?: boolean;
allowTelemetry?: boolean; allowTelemetry?: boolean;
downloadMirror?: string | "default"; downloadMirror?: string | "default";
alwaysUseBundledKubectl?: boolean;
} }
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {

View File

@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl" import { Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
export interface KubeAuthProxyLog { export interface KubeAuthProxyLog {
@ -23,7 +23,7 @@ export class KubeAuthProxy {
this.env = env this.env = env
this.port = port this.port = port
this.cluster = cluster this.cluster = cluster
this.kubectl = bundledKubectl this.kubectl = Kubectl.bundled
} }
public async run(): Promise<void> { public async run(): Promise<void> {

View File

@ -11,7 +11,8 @@ import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version" import { getBundledKubectlVersion } from "../common/utils/app-version"
import { isDevelopment, isWindows } from "../common/vars"; import { isDevelopment, isWindows } from "../common/vars";
const bundledVersion = getBundledKubectlVersion() // kubectlMap maps the "Major.Minor" version of kubernetes to a
// "Major.Minor.Patch" version of kubectl
const kubectlMap: Map<string, string> = new Map([ const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"], ["1.7", "1.8.15"],
["1.8", "1.9.10"], ["1.8", "1.9.10"],
@ -23,7 +24,7 @@ const kubectlMap: Map<string, string> = new Map([
["1.14", "1.14.10"], ["1.14", "1.14.10"],
["1.15", "1.15.11"], ["1.15", "1.15.11"],
["1.16", "1.16.8"], ["1.16", "1.16.8"],
["1.17", bundledVersion], ["1.17", getBundledKubectlVersion()],
["1.18", "1.18.0"] ["1.18", "1.18.0"]
]) ])
@ -32,70 +33,169 @@ const packageMirrors: Map<string, string> = new Map([
["china", "https://mirror.azure.cn/kubernetes/kubectl"] ["china", "https://mirror.azure.cn/kubernetes/kubectl"]
]) ])
let bundledPath: string const initScriptVersionString = "# lens-initscript v3";
const initScriptVersionString = "# lens-initscript v3\n" const binaryName = isWindows ? "kubectl.exe" : "kubectl";
const platformName = isWindows ? "windows" : process.platform;
const normalizedArch = normalizeArch();
const bashInitScriptFileName = ".bash_set_path";
const zshInitScriptFileName = ".zlogin";
if (isDevelopment) { function normalizeArch() {
bundledPath = path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl") if (process.arch == "x64") {
} else { return "amd64";
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") }
if (["x86", "ia32"].includes(process.arch)) {
return "386";
}
return process.arch;
} }
if (isWindows) { function renderBundledPath() {
bundledPath = `${bundledPath}.exe` const pathParts = [];
if (isDevelopment) {
pathParts.push(process.cwd(), "binaries", "client", process.platform);
} else {
pathParts.push(process.resourcesPath);
}
pathParts.push(process.arch, binaryName);
return path.join(...pathParts);
}
async function scriptIsLatest(scriptPath: string) {
if (!await pathExists(scriptPath)) {
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
}
}
async function ensureLatestBashInitScript(helmPath: string, dirname: string) {
const bashScriptPath = path.join(dirname, bashInitScriptFileName)
if (await scriptIsLatest(bashScriptPath)) {
return;
}
const bashScript =
`${initScriptVersionString}
tempkubeconfig="$KUBECONFIG"
test -f "/etc/profile" && . "/etc/profile"
if test -f "$HOME/.bash_profile"; then
. "$HOME/.bash_profile"
elif test -f "$HOME/.bash_login"; then
. "$HOME/.bash_login"
elif test -f "$HOME/.profile"; then
. "$HOME/.profile"
fi
export PATH="${this.dirname}:${helmPath}:$PATH"
export KUBECONFIG="$tempkubeconfig"
unset tempkubeconfig
`;
return fs.promises.writeFile(bashScriptPath, bashScript, { mode: 0o644 })
}
async function ensureLatestZshInitScript(helmPath: string, dirname: string) {
const zshScriptPath = path.join(dirname, zshInitScriptFileName)
if (await scriptIsLatest(zshScriptPath)) {
return;
}
const zshScript =
`${initScriptVersionString}
tempkubeconfig=\"$KUBECONFIG\"
# restore previous ZDOTDIR
export ZDOTDIR="$OLD_ZDOTDIR"
# source all the files
"test -f "$OLD_ZDOTDIR/.zshenv" && . "$OLD_ZDOTDIR/.zshenv"
test -f "$OLD_ZDOTDIR/.zprofile" && . "$OLD_ZDOTDIR/.zprofile"
test -f "$OLD_ZDOTDIR/.zlogin" && . "$OLD_ZDOTDIR/.zlogin"
test -f "$OLD_ZDOTDIR/.zshrc" && . "$OLD_ZDOTDIR/.zshrc"
# voodoo to replace any previous occurences of kubectl path in the PATH
kubectlpath="${this.dirname}"
helmpath="${helmPath}"
p=":$kubectlpath:"
d=":$PATH:"
d=\${d//$p/:}
d=\${ d /#: /}
export PATH="$kubectlpath:$helmpath:\${d/%:/}"
export KUBECONFIG ="$tempkubeconfig"
unset tempkubeconfig
unset OLD_ZDOTDIR
`;
return fs.promises.writeFile(zshScriptPath, zshScript, { mode: 0o644 })
}
function getDownloadMirror() {
// MacOS packages are only available from default
return packageMirrors.get(userStore.preferences.downloadMirror)
|| packageMirrors.get("default");
}
interface ClientOnlyKubeVersion {
clientVersion: {
major: string;
minor: string;
gitVersion: string;
gitCommit: string;
gitTreeState: string;
buildDate: string;
goVersion: string;
compiler: string;
platform: string;
}
} }
export class Kubectl { export class Kubectl {
public kubectlVersion: string private _kubectlVersion: string
protected directory: string public get kubectlVersion() {
protected url: string return userStore.preferences.alwaysUseBundledKubectl
protected path: string ? getBundledKubectlVersion()
protected dirname: string : this._kubectlVersion
static get kubectlDir() {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
} }
public static readonly bundledKubectlPath = bundledPath public readonly url: string
public static readonly bundledKubectlVersion: string = bundledVersion public readonly path: string
private static bundledInstance: Kubectl; public get dirname() {
return path.dirname(this.path);
// Returns the single bundled Kubectl instance
public static bundled() {
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
return Kubectl.bundledInstance
} }
public static readonly defaultDirectory = path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
public static readonly bundled: Kubectl = Object.create(Kubectl.prototype, {
kubectlVersion: { value: getBundledKubectlVersion() },
url: { value: `${getDownloadMirror()}/v${getBundledKubectlVersion()}/bin/${platformName}/${normalizedArch}/${binaryName}` },
path: { value: renderBundledPath() },
});
constructor(clusterVersion: string) { constructor(clusterVersion: string) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion) const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion)
const minorVersion = versionParts[1] const majorMinorVersion = versionParts[1]
/* minorVersion is the first two digits of kube server version const versionLocation = kubectlMap.has(majorMinorVersion) ? "version map" : "fallback";
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if (kubectlMap.has(minorVersion)) {
this.kubectlVersion = kubectlMap.get(minorVersion)
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map")
} else {
this.kubectlVersion = versionParts[1] + versionParts[2]
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback")
}
let arch = null // if kubectlMap has the "Major.Minor" kube version, use the associated semver kubectl version
// otherwise use the exact version given.
this._kubectlVersion = kubectlMap.get(majorMinorVersion) || (versionParts[1] + versionParts[2]);
logger.debug(`Set kubectl version ${this._kubectlVersion} for cluster version ${clusterVersion} using ${versionLocation}`)
if (process.arch == "x64") { this.url = `${getDownloadMirror()}/v${this._kubectlVersion}/bin/${platformName}/${normalizedArch}/${binaryName}`
arch = "amd64" this.path = path.normalize(path.join(Kubectl.defaultDirectory, this._kubectlVersion, binaryName))
} 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.dirname = path.normalize(path.join(Kubectl.kubectlDir, this.kubectlVersion))
this.path = path.join(this.dirname, binaryName)
} }
public async getPath(): Promise<string> { public async getPath(): Promise<string> {
@ -105,88 +205,89 @@ export class Kubectl {
} catch (err) { } catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version") logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err) logger.error(err)
return Kubectl.bundledKubectlPath return Kubectl.bundled.path
} }
} }
public async binDir() { public async binDir() {
try { return path.dirname(await this.getPath());
await this.ensureKubectl()
return this.dirname
} catch (err) {
logger.error(err)
return ""
}
} }
public async checkBinary(checkVersion = true) { private async binaryIsCorrectVersion() {
const exists = await pathExists(this.path) if (!await pathExists(this.path)) {
if (exists) { return false;
if (!checkVersion) { }
try {
const { stdout } = await promiseExec(`"${this.path}" version --client=true -o json`)
const output: ClientOnlyKubeVersion = JSON.parse(stdout)
const version = /^v?(.+)/g.exec(output.clientVersion.gitVersion)[1];
if (version === this.kubectlVersion) {
logger.debug(`Local kubectl is version ${this.kubectlVersion}`)
return true return true
} }
try { logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`)
const { stdout } = await promiseExec(`"${this.path}" version --client=true -o json`) } catch (err) {
const output = JSON.parse(stdout) logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`)
let version: string = output.clientVersion.gitVersion
if (version[0] === 'v') {
version = version.slice(1)
}
if (version === this.kubectlVersion) {
logger.debug(`Local kubectl is version ${this.kubectlVersion}`)
return true
}
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`)
} catch (err) {
logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`)
}
await fs.promises.unlink(this.path)
} }
await fs.promises.unlink(this.path)
return false return false
} }
protected async checkBundled(): Promise<boolean> { private async binaryIsBundled(): Promise<boolean> {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) { if (this.kubectlVersion !== Kubectl.bundled.kubectlVersion) {
try { return false;
const exist = await pathExists(this.path) }
if (!exist) {
await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path) try {
await fs.promises.chmod(this.path, 0o755) if (!await pathExists(this.path)) {
} await fs.promises.copyFile(Kubectl.bundled.path, this.path)
return true await fs.promises.chmod(this.path, 0o755)
} catch (err) {
logger.error("Could not copy the bundled kubectl to app-data: " + err)
return false
} }
} else {
return true
} catch (err) {
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`)
return false return false
} }
} }
public async ensureKubectl(): Promise<boolean> { public async ensureKubectl(): Promise<boolean> {
await ensureDir(this.dirname, 0o755) await ensureDir(this.dirname, 0o755)
return lockFile.lock(this.dirname).then(async (release) => {
try {
const release = await lockFile.lock(this.dirname);
logger.debug(`Acquired a lock for ${this.kubectlVersion}`) logger.debug(`Acquired a lock for ${this.kubectlVersion}`)
const bundled = await this.checkBundled()
const isValid = await this.checkBinary(!bundled) if (!await this.binaryIsBundled()
if (!isValid) { && !await this.binaryIsCorrectVersion()) {
await this.downloadKubectl().catch((error) => { try {
logger.error(error) await this.downloadKubectl();
}); } catch (err) {
logger.error("Failed to write init scripts");
logger.error(err);
}
} }
await this.writeInitScripts().catch((error) => {
try {
await this.ensureLatestInitScripts();
} catch (err) {
logger.error("Failed to write init scripts"); logger.error("Failed to write init scripts");
logger.error(error) logger.error(err)
}) }
logger.debug(`Releasing lock for ${this.kubectlVersion}`) logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release() await release()
return true return true
}).catch((e) => { } catch (e) {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`) logger.error(`Failed to get a lock for ${this.kubectlVersion}`)
logger.error(e) logger.error(e)
return false return false;
}) }
} }
public async downloadKubectl() { public async downloadKubectl() {
@ -221,81 +322,13 @@ export class Kubectl {
}) })
} }
protected async scriptIsLatest(scriptPath: string) { protected async ensureLatestInitScripts() {
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 helmPath = helmCli.getBinaryDir() const helmPath = helmCli.getBinaryDir()
const fsPromises = fs.promises; const dirname = this.dirname;
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 })
}
const zshScriptPath = path.join(this.dirname, '.zlogin') return Promise.all([
const zshScriptIsLatest = await this.scriptIsLatest(zshScriptPath) ensureLatestBashInitScript(helmPath, dirname),
if (!zshScriptIsLatest) { ensureLatestZshInitScript(helmPath, dirname),
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 })
}
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror)
if (mirror) {
return mirror
}
return packageMirrors.get("default") // MacOS packages are only available from default
} }
} }
const bundledKubectl = Kubectl.bundled()
export { bundledKubectl }

View File

@ -1,17 +1,17 @@
import packageInfo from "../../package.json" import packageInfo from "../../package.json"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl"; import { Kubectl } from "../../src/main/kubectl";
jest.mock("../common/user-store"); jest.mock("../common/user-store");
describe("kubectlVersion", () => { describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => { it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion) const kubectl = new Kubectl(Kubectl.bundled.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) expect(kubectl.kubectlVersion).toBe(Kubectl.bundled.kubectlVersion)
}) })
it("returns bundled version if same major.minor version is used", async () => { it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config; const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion); const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) expect(kubectl.kubectlVersion).toBe(Kubectl.bundled.kubectlVersion)
}) })
}) })

View File

@ -1,7 +1,7 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process" import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { bundledKubectl } from "../kubectl" import { Kubectl } from "../kubectl"
import { getFreePort } from "../port" import { getFreePort } from "../port"
import { shell } from "electron" import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used" import * as tcpPortUsed from "tcp-port-used"
@ -37,7 +37,7 @@ class PortForward {
public async start() { public async start() {
this.localPort = await getFreePort() this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.getPath() const kubectlBin = await Kubectl.bundled.getPath()
const args = [ const args = [
"--kubeconfig", this.kubeConfig, "--kubeconfig", this.kubeConfig,
"port-forward", "port-forward",

View File

@ -99,7 +99,7 @@ export class Preferences extends React.Component {
return ( return (
<div className="flex gaps"> <div className="flex gaps">
<span>{repo.name}</span> <span>{repo.name}</span>
{isAdded && <Icon small material="check" className="box right"/>} {isAdded && <Icon small material="check" className="box right" />}
</div> </div>
) )
} }
@ -109,7 +109,7 @@ export class Preferences extends React.Component {
const header = ( const header = (
<> <>
<h2>Preferences</h2> <h2>Preferences</h2>
<Icon material="close" big onClick={history.goBack}/> <Icon material="close" big onClick={history.goBack} />
</> </>
); );
return ( return (
@ -122,6 +122,14 @@ export class Preferences extends React.Component {
onChange={({ value }: SelectOption) => preferences.colorTheme = value} onChange={({ value }: SelectOption) => preferences.colorTheme = value}
/> />
<h2><Trans>Kubectl Binary</Trans></h2>
<Checkbox
value={preferences.alwaysUseBundledKubectl}
onChange={(value) => preferences.alwaysUseBundledKubectl = value}
>
Always use the bundled kubectl binary. Never try and download a newer version.
</Checkbox>
<h2><Trans>Download Mirror</Trans></h2> <h2><Trans>Download Mirror</Trans></h2>
<Select <Select
placeholder={<Trans>Download mirror for kubectl</Trans>} placeholder={<Trans>Download mirror for kubectl</Trans>}

View File

@ -64,6 +64,7 @@
color: var(--checkbox-color); color: var(--checkbox-color);
border: 2px solid currentColor; border: 2px solid currentColor;
flex-shrink: 0; flex-shrink: 0;
margin-right: 10px;
&:after { &:after {
content: ""; content: "";