1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/main/kubectl.ts
Jari Kolehmainen 78f6f8ef54
Fix bundled kubectl path on dev env (#981)
* fix bundled kubectl path on dev env

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix specs

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
2020-10-14 14:35:33 +03:00

340 lines
12 KiB
TypeScript

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<string, string> = 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<string, string> = 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<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
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<boolean> {
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<boolean> {
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
}
}