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;
allowTelemetry?: boolean;
downloadMirror?: string | "default";
alwaysUseBundledKubectl?: boolean;
}
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 { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl"
import { Kubectl } from "./kubectl"
import logger from "./logger"
export interface KubeAuthProxyLog {
@ -23,7 +23,7 @@ export class KubeAuthProxy {
this.env = env
this.port = port
this.cluster = cluster
this.kubectl = bundledKubectl
this.kubectl = Kubectl.bundled
}
public async run(): Promise<void> {

View File

@ -11,7 +11,8 @@ import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version"
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([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
@ -23,7 +24,7 @@ const kubectlMap: Map<string, string> = new Map([
["1.14", "1.14.10"],
["1.15", "1.15.11"],
["1.16", "1.16.8"],
["1.17", bundledVersion],
["1.17", getBundledKubectlVersion()],
["1.18", "1.18.0"]
])
@ -32,70 +33,169 @@ const packageMirrors: Map<string, string> = new Map([
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
])
let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n"
const initScriptVersionString = "# lens-initscript v3";
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) {
bundledPath = path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl")
} else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
function normalizeArch() {
if (process.arch == "x64") {
return "amd64";
}
if (["x86", "ia32"].includes(process.arch)) {
return "386";
}
return process.arch;
}
if (isWindows) {
bundledPath = `${bundledPath}.exe`
function renderBundledPath() {
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 {
public kubectlVersion: string
protected directory: string
protected url: string
protected path: string
protected dirname: string
static get kubectlDir() {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
private _kubectlVersion: string
public get kubectlVersion() {
return userStore.preferences.alwaysUseBundledKubectl
? getBundledKubectlVersion()
: this._kubectlVersion
}
public static readonly bundledKubectlPath = bundledPath
public static readonly bundledKubectlVersion: string = bundledVersion
private static bundledInstance: Kubectl;
// Returns the single bundled Kubectl instance
public static bundled() {
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
return Kubectl.bundledInstance
public readonly url: string
public readonly path: string
public get dirname() {
return path.dirname(this.path);
}
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) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion)
const minorVersion = versionParts[1]
/* minorVersion is the first two digits of kube server version
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")
}
const majorMinorVersion = versionParts[1]
const versionLocation = kubectlMap.has(majorMinorVersion) ? "version map" : "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") {
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.dirname = path.normalize(path.join(Kubectl.kubectlDir, this.kubectlVersion))
this.path = path.join(this.dirname, binaryName)
this.url = `${getDownloadMirror()}/v${this._kubectlVersion}/bin/${platformName}/${normalizedArch}/${binaryName}`
this.path = path.normalize(path.join(Kubectl.defaultDirectory, this._kubectlVersion, binaryName))
}
public async getPath(): Promise<string> {
@ -105,88 +205,89 @@ export class Kubectl {
} catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err)
return Kubectl.bundledKubectlPath
return Kubectl.bundled.path
}
}
public async binDir() {
try {
await this.ensureKubectl()
return this.dirname
} catch (err) {
logger.error(err)
return ""
}
return path.dirname(await this.getPath());
}
public async checkBinary(checkVersion = true) {
const exists = await pathExists(this.path)
if (exists) {
if (!checkVersion) {
private async binaryIsCorrectVersion() {
if (!await pathExists(this.path)) {
return false;
}
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
}
try {
const { stdout } = await promiseExec(`"${this.path}" version --client=true -o json`)
const output = JSON.parse(stdout)
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)
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)
return false
}
protected async checkBundled(): Promise<boolean> {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
const exist = await pathExists(this.path)
if (!exist) {
await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path)
await fs.promises.chmod(this.path, 0o755)
}
return true
} catch (err) {
logger.error("Could not copy the bundled kubectl to app-data: " + err)
return false
private async binaryIsBundled(): Promise<boolean> {
if (this.kubectlVersion !== Kubectl.bundled.kubectlVersion) {
return false;
}
try {
if (!await pathExists(this.path)) {
await fs.promises.copyFile(Kubectl.bundled.path, this.path)
await fs.promises.chmod(this.path, 0o755)
}
} else {
return true
} catch (err) {
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`)
return false
}
}
public async ensureKubectl(): Promise<boolean> {
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}`)
const bundled = await this.checkBundled()
const isValid = await this.checkBinary(!bundled)
if (!isValid) {
await this.downloadKubectl().catch((error) => {
logger.error(error)
});
if (!await this.binaryIsBundled()
&& !await this.binaryIsCorrectVersion()) {
try {
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(error)
})
logger.error(err)
}
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
await release()
return true
}).catch((e) => {
} catch (e) {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`)
logger.error(e)
return false
})
return false;
}
}
public async downloadKubectl() {
@ -221,81 +322,13 @@ 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() {
protected async ensureLatestInitScripts() {
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 })
}
const dirname = this.dirname;
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 })
}
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror)
if (mirror) {
return mirror
}
return packageMirrors.get("default") // MacOS packages are only available from default
return Promise.all([
ensureLatestBashInitScript(helmPath, dirname),
ensureLatestZshInitScript(helmPath, dirname),
]);
}
}
const bundledKubectl = Kubectl.bundled()
export { bundledKubectl }

View File

@ -1,17 +1,17 @@
import packageInfo from "../../package.json"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
import { Kubectl } from "../../src/main/kubectl";
jest.mock("../common/user-store");
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
const kubectl = new Kubectl(Kubectl.bundled.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled.kubectlVersion)
})
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
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 { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { bundledKubectl } from "../kubectl"
import { Kubectl } from "../kubectl"
import { getFreePort } from "../port"
import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used"
@ -37,7 +37,7 @@ class PortForward {
public async start() {
this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.getPath()
const kubectlBin = await Kubectl.bundled.getPath()
const args = [
"--kubeconfig", this.kubeConfig,
"port-forward",

View File

@ -99,7 +99,7 @@ export class Preferences extends React.Component {
return (
<div className="flex gaps">
<span>{repo.name}</span>
{isAdded && <Icon small material="check" className="box right"/>}
{isAdded && <Icon small material="check" className="box right" />}
</div>
)
}
@ -109,7 +109,7 @@ export class Preferences extends React.Component {
const header = (
<>
<h2>Preferences</h2>
<Icon material="close" big onClick={history.goBack}/>
<Icon material="close" big onClick={history.goBack} />
</>
);
return (
@ -122,6 +122,14 @@ export class Preferences extends React.Component {
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>
<Select
placeholder={<Trans>Download mirror for kubectl</Trans>}

View File

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