mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
* Move downloading binaries to new package Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove old location from files Signed-off-by: Sebastian Malton <sebastian@malton.name> * Swtich @k8slens/download-binaries to provide a binary Signed-off-by: Sebastian Malton <sebastian@malton.name> --------- Signed-off-by: Sebastian Malton <sebastian@malton.name>
299 lines
8.4 KiB
JavaScript
299 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
*/
|
|
import { FileHandle, readFile } from "fs/promises";
|
|
import type { WriteStream } from "fs";
|
|
import { constants } from "fs";
|
|
import { open, mkdir, unlink } from "fs/promises";
|
|
import path from "path";
|
|
import { promisify } from "util";
|
|
import { pipeline as _pipeline, Transform, Writable } from "stream";
|
|
import type { SingleBar } from "cli-progress";
|
|
import { MultiBar } from "cli-progress";
|
|
import { extract } from "tar-stream";
|
|
import gunzip from "gunzip-maybe";
|
|
import fetch from "node-fetch"
|
|
import z from "zod";
|
|
import arg from "arg";
|
|
|
|
const options = arg({
|
|
"--package": String,
|
|
"--base-dir": String,
|
|
});
|
|
|
|
const pathToPackage = options["--package"];
|
|
const pathToBaseDir = options["--base-dir"];
|
|
|
|
if (typeof pathToPackage !== "string") {
|
|
throw new Error("--package is required");
|
|
}
|
|
|
|
if (typeof pathToBaseDir !== "string") {
|
|
throw new Error("--base-dir is required");
|
|
}
|
|
|
|
function setTimeoutFor(controller: AbortController, timeout: number): void {
|
|
const handle = setTimeout(() => controller.abort(), timeout);
|
|
|
|
controller.signal.addEventListener("abort", () => clearTimeout(handle));
|
|
}
|
|
|
|
const pipeline = promisify(_pipeline);
|
|
|
|
const getBinaryName = (binaryName: string, { forPlatform }: { forPlatform : string }) => {
|
|
if (forPlatform === "windows") {
|
|
return `${binaryName}.exe`;
|
|
}
|
|
|
|
return binaryName;
|
|
};
|
|
|
|
interface BinaryDownloaderArgs {
|
|
readonly version: string;
|
|
readonly platform: SupportedPlatform;
|
|
readonly downloadArch: string;
|
|
readonly fileArch: string;
|
|
readonly binaryName: string;
|
|
readonly baseDir: string;
|
|
}
|
|
|
|
abstract class BinaryDownloader {
|
|
protected abstract readonly url: string;
|
|
protected readonly bar: SingleBar;
|
|
protected readonly target: string;
|
|
|
|
protected getTransformStreams(file: Writable): (NodeJS.ReadWriteStream | NodeJS.WritableStream)[] {
|
|
return [file];
|
|
}
|
|
|
|
constructor(public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) {
|
|
this.bar = multiBar.create(1, 0, args);
|
|
this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName);
|
|
}
|
|
|
|
async ensureBinary(): Promise<void> {
|
|
if (process.env.LENS_SKIP_DOWNLOAD_BINARIES === "true") {
|
|
return;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
|
|
setTimeoutFor(controller, 15 * 60 * 1000);
|
|
|
|
const stream = await fetch(this.url, {
|
|
signal: controller.signal,
|
|
});
|
|
const total = Number(stream.headers.get("content-length"));
|
|
const bar = this.bar;
|
|
let fileHandle: FileHandle | undefined = undefined;
|
|
|
|
if (isNaN(total)) {
|
|
throw new Error("no content-length header was present");
|
|
}
|
|
|
|
bar.setTotal(total);
|
|
|
|
await mkdir(path.dirname(this.target), {
|
|
mode: 0o755,
|
|
recursive: true,
|
|
});
|
|
|
|
try {
|
|
/**
|
|
* This is necessary because for some reason `createWriteStream({ flags: "wx" })`
|
|
* was throwing someplace else and not here
|
|
*/
|
|
const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
|
|
|
|
if (!stream.body) {
|
|
throw new Error("no body on stream");
|
|
}
|
|
|
|
await pipeline(
|
|
stream.body,
|
|
new Transform({
|
|
transform(chunk, encoding, callback) {
|
|
bar.increment(chunk.length);
|
|
this.push(chunk);
|
|
callback();
|
|
},
|
|
}),
|
|
...this.getTransformStreams(new Writable({
|
|
write(chunk, encoding, cb) {
|
|
handle.write(chunk)
|
|
.then(() => cb())
|
|
.catch(cb);
|
|
},
|
|
})),
|
|
);
|
|
await fileHandle.chmod(0o755);
|
|
await fileHandle.close();
|
|
} catch (error) {
|
|
await fileHandle?.close();
|
|
|
|
if ((error as any)?.code === "EEXIST") {
|
|
bar.increment(total); // mark as finished
|
|
controller.abort(); // stop trying to download
|
|
} else {
|
|
await unlink(this.target);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class LensK8sProxyDownloader extends BinaryDownloader {
|
|
protected readonly url: string;
|
|
|
|
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
|
const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform });
|
|
|
|
super({ ...args, binaryName }, bar);
|
|
this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`;
|
|
}
|
|
}
|
|
|
|
class KubectlDownloader extends BinaryDownloader {
|
|
protected readonly url: string;
|
|
|
|
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
|
const binaryName = getBinaryName("kubectl", { forPlatform: args.platform });
|
|
|
|
super({ ...args, binaryName }, bar);
|
|
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`;
|
|
}
|
|
}
|
|
|
|
class HelmDownloader extends BinaryDownloader {
|
|
protected readonly url: string;
|
|
|
|
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
|
const binaryName = getBinaryName("helm", { forPlatform: args.platform });
|
|
|
|
super({ ...args, binaryName }, bar);
|
|
this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`;
|
|
}
|
|
|
|
protected getTransformStreams(file: WriteStream) {
|
|
const extracting = extract({
|
|
allowUnknownFormat: false,
|
|
});
|
|
|
|
extracting.on("entry", (headers, stream, next) => {
|
|
if (headers.name.endsWith(this.args.binaryName)) {
|
|
stream
|
|
.pipe(file)
|
|
.once("finish", () => next())
|
|
.once("error", next);
|
|
} else {
|
|
stream.resume();
|
|
next();
|
|
}
|
|
});
|
|
|
|
return [gunzip(3), extracting];
|
|
}
|
|
}
|
|
|
|
type SupportedPlatform = "darwin" | "linux" | "windows";
|
|
|
|
const PackageInfo = z.object({
|
|
config: z.object({
|
|
k8sProxyVersion: z.string().min(1),
|
|
bundledKubectlVersion: z.string().min(1),
|
|
bundledHelmVersion: z.string().min(1),
|
|
})
|
|
})
|
|
|
|
const packageInfoRaw = await readFile(pathToPackage, "utf-8");
|
|
const packageInfo = PackageInfo.parse(JSON.parse(packageInfoRaw));
|
|
|
|
const normalizedPlatform = (() => {
|
|
switch (process.platform) {
|
|
case "darwin":
|
|
return "darwin";
|
|
case "linux":
|
|
return "linux";
|
|
case "win32":
|
|
return "windows";
|
|
default:
|
|
throw new Error(`platform=${process.platform} is unsupported`);
|
|
}
|
|
})();
|
|
const multiBar = new MultiBar({
|
|
align: "left",
|
|
clearOnComplete: false,
|
|
hideCursor: true,
|
|
autopadding: true,
|
|
noTTYOutput: true,
|
|
format: "[{bar}] {percentage}% | {downloadArch} {binaryName}",
|
|
});
|
|
const downloaders: BinaryDownloader[] = [
|
|
new LensK8sProxyDownloader({
|
|
version: packageInfo.config.k8sProxyVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "amd64",
|
|
fileArch: "x64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
new KubectlDownloader({
|
|
version: packageInfo.config.bundledKubectlVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "amd64",
|
|
fileArch: "x64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
new HelmDownloader({
|
|
version: packageInfo.config.bundledHelmVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "amd64",
|
|
fileArch: "x64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
];
|
|
|
|
if (normalizedPlatform !== "windows") {
|
|
downloaders.push(
|
|
new LensK8sProxyDownloader({
|
|
version: packageInfo.config.k8sProxyVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "arm64",
|
|
fileArch: "arm64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
new KubectlDownloader({
|
|
version: packageInfo.config.bundledKubectlVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "arm64",
|
|
fileArch: "arm64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
new HelmDownloader({
|
|
version: packageInfo.config.bundledHelmVersion,
|
|
platform: normalizedPlatform,
|
|
downloadArch: "arm64",
|
|
fileArch: "arm64",
|
|
baseDir: pathToBaseDir,
|
|
}, multiBar),
|
|
);
|
|
}
|
|
|
|
const settledResults = await Promise.allSettled(downloaders.map(downloader => (
|
|
downloader.ensureBinary()
|
|
.catch(error => {
|
|
throw new Error(`Failed to download ${downloader.args.binaryName} for ${downloader.args.platform}/${downloader.args.downloadArch}: ${error}`);
|
|
})
|
|
)));
|
|
|
|
multiBar.stop();
|
|
const errorResult = settledResults.find(res => res.status === "rejected") as PromiseRejectedResult | undefined;
|
|
|
|
if (errorResult) {
|
|
console.error("234", String(errorResult.reason));
|
|
process.exit(1);
|
|
}
|
|
|
|
process.exit(0);
|