import { app, remote } from "electron"; import path from "path"; import fs from "fs"; import { promiseExec } from "./promise-exec"; import logger from "./logger"; import { ensureDir, pathExists } from "fs-extra"; import * as lockFile from "proper-lockfile"; import { helmCli } from "./helm/helm-cli"; import { userStore } from "../common/user-store"; import { customRequest } from "../common/request"; import { getBundledKubectlVersion } from "../common/utils/app-version"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ ["1.7", "1.8.15"], ["1.8", "1.9.10"], ["1.9", "1.10.13"], ["1.10", "1.11.10"], ["1.11", "1.12.10"], ["1.12", "1.13.12"], ["1.13", "1.13.12"], ["1.14", "1.14.10"], ["1.15", "1.15.11"], ["1.16", "1.16.14"], ["1.17", bundledVersion], ["1.18", "1.18.8"], ["1.19", "1.19.0"] ]); const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], ["china", "https://mirror.azure.cn/kubernetes/kubectl"] ]); let bundledPath: string; const initScriptVersionString = "# lens-initscript v3\n"; export function bundledKubectlPath(): string { if (bundledPath) { return bundledPath; } if (isDevelopment || isTestEnv) { const platformName = isWindows ? "windows" : process.platform; bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl"); } else { bundledPath = path.join(process.resourcesPath, process.arch, "kubectl"); } if (isWindows) { bundledPath = `${bundledPath}.exe`; } return bundledPath; } 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"); } public static readonly bundledKubectlVersion: string = bundledVersion public static invalidBundle = false 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; } 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"); } let arch = null; 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(this.getDownloadDir(), this.kubectlVersion)); this.path = path.join(this.dirname, binaryName); } public getBundledPath() { return bundledKubectlPath(); } public getPathFromPreferences() { return userStore.preferences?.kubectlBinariesPath || this.getBundledPath(); } protected getDownloadDir() { if (userStore.preferences?.downloadBinariesPath) { return path.join(userStore.preferences.downloadBinariesPath, "kubectl"); } return 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; return path.basename(this.getBundledPath()); } try { if (!await this.ensureKubectl()) { logger.error("Failed to ensure kubectl, fallback to the bundled version"); return this.getBundledPath(); } return this.path; } catch (err) { logger.error("Failed to ensure kubectl, fallback to the bundled version"); logger.error(err); return this.getBundledPath(); } } public async binDir() { try { await this.ensureKubectl(); await this.writeInitScripts(); return this.dirname; } catch (err) { logger.error(err); return ""; } } public async checkBinary(path: string, checkVersion = true) { const exists = await pathExists(path); if (exists) { try { const { stdout } = await promiseExec(`"${path}" version --client=true -o json`); const output = JSON.parse(stdout); if (!checkVersion) { return true; } 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); } return false; } protected async checkBundled(): Promise { if (this.kubectlVersion === Kubectl.bundledKubectlVersion) { try { const exist = await pathExists(this.path); if (!exist) { await fs.promises.copyFile(this.getBundledPath(), 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; } } else { return false; } } public async ensureKubectl(): Promise { if (userStore.preferences?.downloadKubectlBinaries === false) { return true; } if (Kubectl.invalidBundle) { logger.error(`Detected invalid bundle binary, returning ...`); return false; } await ensureDir(this.dirname, 0o755); return lockFile.lock(this.dirname).then(async (release) => { logger.debug(`Acquired a lock for ${this.kubectlVersion}`); const bundled = await this.checkBundled(); let isValid = await this.checkBinary(this.path, !bundled); if (!isValid && !bundled) { await this.downloadKubectl().catch((error) => { logger.error(error); logger.debug(`Releasing lock for ${this.kubectlVersion}`); release(); return false; }); isValid = !await this.checkBinary(this.path, false); } if (!isValid) { logger.debug(`Releasing lock for ${this.kubectlVersion}`); release(); return false; } logger.debug(`Releasing lock for ${this.kubectlVersion}`); release(); return true; }).catch((e) => { logger.error(`Failed to get a lock for ${this.kubectlVersion}`); logger.error(e); return false; }); } public async downloadKubectl() { await ensureDir(path.dirname(this.path), 0o755); logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); return new Promise((resolve, reject) => { const stream = customRequest({ url: this.url, gzip: true, }); const file = fs.createWriteStream(this.path); stream.on("complete", () => { logger.debug("kubectl binary download finished"); file.end(); }); stream.on("error", (error) => { logger.error(error); fs.unlink(this.path, () => { // do nothing }); reject(error); }); file.on("close", () => { logger.debug("kubectl binary download closed"); fs.chmod(this.path, 0o755, (err) => { if (err) reject(err); }); resolve(); }); stream.pipe(file); }); } 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'); 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 += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"; bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"; bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"; bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"; bashScript += "export NO_PROXY\n"; bashScript += "unset tempkubeconfig\n"; await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }); const zshScriptPath = path.join(this.dirname, '.zlogin'); 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 += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"; zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"; zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"; zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"; zshScript += "export NO_PROXY\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 } }