From ff4222a0bda9ba62e05cea739bd23320c5ae471c Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 28 Feb 2022 16:37:29 +0200 Subject: [PATCH] Replace kubectl-proxy with lens-k8s-proxy (#4924) Signed-off-by: DmitriyNoa --- build/download_k8s_proxy.ts | 98 +++++++++++++++++++ package.json | 18 ++++ ...rectory-for-bundled-binaries.injectable.ts | 22 +++++ src/common/vars/context-dir.injectable.ts | 14 +++ src/common/vars/is-development.injectable.ts | 14 +++ src/main/__test__/kube-auth-proxy.test.ts | 5 +- src/main/context-handler/context-handler.ts | 2 +- src/main/getDiForUnitTesting.ts | 2 + .../create-kube-auth-proxy.injectable.ts | 10 +- src/main/kube-auth-proxy/kube-auth-proxy.ts | 37 +++---- 10 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 build/download_k8s_proxy.ts create mode 100644 src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts create mode 100644 src/common/vars/context-dir.injectable.ts create mode 100644 src/common/vars/is-development.injectable.ts diff --git a/build/download_k8s_proxy.ts b/build/download_k8s_proxy.ts new file mode 100644 index 0000000000..891f4f573c --- /dev/null +++ b/build/download_k8s_proxy.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import packageInfo from "../package.json"; +import fs from "fs"; +import request from "request"; +import { ensureDir, pathExists } from "fs-extra"; +import path from "path"; +import { noop } from "lodash"; +import { isLinux, isMac } from "../src/common/vars"; + +class K8sProxyDownloader { + public version: string; + protected url: string; + protected path: string; + protected dirname: string; + + constructor(version: string, platform: string, arch: string, target: string) { + this.version = version; + this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${this.version}/lens-k8s-proxy-${platform}-${arch}`; + this.dirname = path.dirname(target); + this.path = target; + } + + public async checkBinary() { + const exists = await pathExists(this.path); + + if (exists) { + return true; + } + + return false; + } + + public async download() { + if (await this.checkBinary()) { + return console.log("Already exists"); + } + + await ensureDir(path.dirname(this.path), 0o755); + + const file = fs.createWriteStream(this.path); + + console.log(`Downloading lens-k8s-proxy ${this.version} from ${this.url} to ${this.path}`); + const requestOpts: request.UriOptions & request.CoreOptions = { + uri: this.url, + gzip: true, + followAllRedirects: true, + }; + const stream = request(requestOpts); + + stream.on("complete", () => { + console.log("lens-k8s-proxy binary download finished"); + file.end(noop); + }); + + stream.on("error", (error) => { + console.log(error); + fs.unlink(this.path, noop); + throw error; + }); + + return new Promise((resolve, reject) => { + file.on("close", () => { + console.log("lens-k8s-proxy binary download closed"); + fs.chmod(this.path, 0o755, (err) => { + if (err) reject(err); + }); + resolve(); + }); + stream.pipe(file); + }); + } +} +const downloadVersion = packageInfo.config.k8sProxyVersion; +const baseDir = path.join(__dirname, "..", "binaries", "client"); + +const downloads = []; + +if (isMac) { + downloads.push({ platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "lens-k8s-proxy") }); + downloads.push({ platform: "darwin", arch: "arm64", target: path.join(baseDir, "darwin", "arm64", "lens-k8s-proxy") }); +} else if (isLinux) { + downloads.push({ platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "lens-k8s-proxy") }); + downloads.push({ platform: "linux", arch: "arm64", target: path.join(baseDir, "linux", "arm64", "lens-k8s-proxy") }); +} else { + downloads.push({ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "lens-k8s-proxy.exe") }); + downloads.push({ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "lens-k8s-proxy.exe") }); +} + +downloads.forEach((dlOpts) => { + console.log(dlOpts); + const downloader = new K8sProxyDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target); + + console.log(`Downloading: ${JSON.stringify(dlOpts)}`); + downloader.download().then(() => downloader.checkBinary().then(() => console.log("Download complete"))); +}); diff --git a/package.json b/package.json index c3dd7a9a9c..54e18da7c6 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "download-bins": "concurrently yarn:download:*", "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts", + "download:k8s-proxy": "yarn run ts-node build/download_k8s_proxy.ts", "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts", "build:theme-vars": "yarn run ts-node build/build_theme_vars.ts", "lint": "PROD=true yarn run eslint --ext js,ts,tsx --max-warnings=0 .", @@ -47,6 +48,7 @@ "postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version" }, "config": { + "k8sProxyVersion": "0.1.2", "bundledKubectlVersion": "1.23.3", "bundledHelmVersion": "3.7.2", "sentryDsn": "" @@ -129,6 +131,10 @@ "from": "binaries/client/linux/${arch}/kubectl", "to": "./${arch}/kubectl" }, + { + "from": "binaries/client/linux/${arch}/lens-k8s-proxy", + "to": "./${arch}/lens-k8s-proxy" + }, { "from": "binaries/client/${arch}/helm3/helm3", "to": "./helm3/helm3" @@ -150,6 +156,10 @@ "from": "binaries/client/darwin/${arch}/kubectl", "to": "./${arch}/kubectl" }, + { + "from": "binaries/client/darwin/${arch}/lens-k8s-proxy", + "to": "./${arch}/lens-k8s-proxy" + }, { "from": "binaries/client/${arch}/helm3/helm3", "to": "./helm3/helm3" @@ -169,6 +179,14 @@ "from": "binaries/client/windows/ia32/kubectl.exe", "to": "./ia32/kubectl.exe" }, + { + "from": "binaries/client/windows/x64/lens-k8s-proxy", + "to": "./x64/lens-k8s-proxy.exe" + }, + { + "from": "binaries/client/windows/ia32/lens-k8s-proxy", + "to": "./ia32/lens-k8s-proxy.exe" + }, { "from": "binaries/client/x64/helm3/helm3.exe", "to": "./helm3/helm3.exe" diff --git a/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts b/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts new file mode 100644 index 0000000000..f1f279e0b8 --- /dev/null +++ b/src/common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import path from "path"; +import isDevelopmentInjectable from "../../vars/is-development.injectable"; +import contextDirInjectable from "../../vars/context-dir.injectable"; + +const directoryForBundledBinariesInjectable = getInjectable({ + id: "directory-for-bundled-binaries", + instantiate: (di) => { + if (di.inject(isDevelopmentInjectable)) { + return path.join(di.inject(contextDirInjectable), "binaries"); + } + + return process.resourcesPath; + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForBundledBinariesInjectable; diff --git a/src/common/vars/context-dir.injectable.ts b/src/common/vars/context-dir.injectable.ts new file mode 100644 index 0000000000..09484cc816 --- /dev/null +++ b/src/common/vars/context-dir.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { contextDir } from "../vars"; + +const contextDirInjectable = getInjectable({ + id: "context-dir", + instantiate: () => contextDir, + lifecycle: lifecycleEnum.singleton, +}); + +export default contextDirInjectable; diff --git a/src/common/vars/is-development.injectable.ts b/src/common/vars/is-development.injectable.ts new file mode 100644 index 0000000000..c6adadd7e0 --- /dev/null +++ b/src/common/vars/is-development.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { isDevelopment } from "../vars"; + +const isDevelopmentInjectable = getInjectable({ + id: "is-development", + instantiate: () => isDevelopment, + lifecycle: lifecycleEnum.singleton, +}); + +export default isDevelopmentInjectable; diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 7a3f4ea082..4c006ecec9 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -38,7 +38,7 @@ import type { Cluster } from "../../common/cluster/cluster"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import { broadcastMessage } from "../../common/ipc"; import { ChildProcess, spawn } from "child_process"; -import { bundledKubectlPath, Kubectl } from "../kubectl/kubectl"; +import { Kubectl } from "../kubectl/kubectl"; import { mock, MockProxy } from "jest-mock-extended"; import { waitUntilUsed } from "tcp-port-used"; import { EventEmitter, Readable } from "stream"; @@ -49,6 +49,7 @@ import mockFs from "mock-fs"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import path from "path"; console = new Console(stdout, stderr); @@ -194,7 +195,7 @@ describe("kube auth proxy tests", () => { return mockedCP.stdout; }); mockSpawn.mockImplementationOnce((command: string): ChildProcess => { - expect(command).toBe(bundledKubectlPath()); + expect(path.basename(command).split(".")[0]).toBe("lens-k8s-proxy"); return mockedCP; }); diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 5287828b5b..ad1ba734b8 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -118,7 +118,7 @@ export class ContextHandler { await this.ensureServer(); const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""; - return `http://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`; + return `http://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix.slice(0, -1)}${path}`; } async getApiTarget(isLongRunningRequest = false): Promise { diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index c5c156fa11..a1e543d9e0 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -14,6 +14,7 @@ import appNameInjectable from "./app-paths/app-name/app-name.injectable"; import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; +import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, @@ -43,6 +44,7 @@ export const getDiForUnitTesting = ( di.override(setElectronAppPathInjectable, () => () => undefined); di.override(appNameInjectable, () => "some-electron-app-name"); di.override(registerChannelInjectable, () => () => undefined); + di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory"); di.override(writeJsonFileInjectable, () => () => { throw new Error("Tried to write JSON file to file system without specifying explicit override."); diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 6a8699cf9e..411cfddc0c 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -5,16 +5,18 @@ import { getInjectable } from "@ogre-tools/injectable"; import { KubeAuthProxy } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; -import bundledKubectlInjectable from "../kubectl/bundled-kubectl.injectable"; +import path from "path"; +import { isDevelopment, isWindows } from "../../common/vars"; +import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; const createKubeAuthProxyInjectable = getInjectable({ id: "create-kube-auth-proxy", instantiate: (di) => { - const bundledKubectl = di.inject(bundledKubectlInjectable); - + const binaryName = isWindows ? "lens-k8s-proxy.exe" : "lens-k8s-proxy"; + const proxyPath = isDevelopment ? path.join("client", process.platform, process.arch) : process.arch; const dependencies = { - getProxyBinPath: bundledKubectl.getPath, + proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), proxyPath, binaryName), }; return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => diff --git a/src/main/kube-auth-proxy/kube-auth-proxy.ts b/src/main/kube-auth-proxy/kube-auth-proxy.ts index 1e96d69980..d8a291b50c 100644 --- a/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -8,18 +8,17 @@ import { waitUntilUsed } from "tcp-port-used"; import { randomBytes } from "crypto"; import type { Cluster } from "../../common/cluster/cluster"; import logger from "../logger"; -import * as url from "url"; import { getPortFrom } from "../utils/get-port"; import { makeObservable, observable, when } from "mobx"; -const startingServeRegex = /^starting to serve on (?
.+)/i; +const startingServeRegex = /starting to serve on (?
.+)/i; interface Dependencies { - getProxyBinPath: () => Promise; + proxyBinPath: string; } export class KubeAuthProxy { - public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`; + public readonly apiPrefix = `/${randomBytes(8).toString("hex")}/`; public get port(): number { return this._port; @@ -27,13 +26,10 @@ export class KubeAuthProxy { protected _port: number; protected proxyProcess?: ChildProcess; - protected readonly acceptHosts: string; @observable protected ready = false; constructor(private dependencies: Dependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { makeObservable(this); - - this.acceptHosts = url.parse(this.cluster.apiUrl).hostname; } get whenReady() { @@ -45,23 +41,16 @@ export class KubeAuthProxy { return this.whenReady; } - const proxyBin = await this.dependencies.getProxyBinPath(); - const args = [ - "proxy", - "-p", "0", - "--kubeconfig", `${this.cluster.kubeConfigPath}`, - "--context", `${this.cluster.contextName}`, - "--accept-hosts", this.acceptHosts, - "--reject-paths", "^[^/]", - "--api-prefix", this.apiPrefix, - ]; - - if (process.env.DEBUG_PROXY === "true") { - args.push("-v", "9"); - } - logger.debug(`spawning kubectl proxy with args: ${args}`); - - this.proxyProcess = spawn(proxyBin, args, { env: this.env }); + const proxyBin = this.dependencies.proxyBinPath; + + this.proxyProcess = spawn(proxyBin, [], { + env: { + ...this.env, + KUBECONFIG: this.cluster.kubeConfigPath, + KUBECONFIG_CONTEXT: this.cluster.contextName, + API_PREFIX: this.apiPrefix, + }, + }); this.proxyProcess.on("error", (error) => { this.cluster.broadcastConnectUpdate(error.message, true); this.exit();