UNPKG

@rm3l/plugin-scaffolder-odo-actions

Version:

odo custom actions for Backstage (Backend Plugin)

217 lines (191 loc) 6.54 kB
"use strict"; // Inspired from https://github.com/ipfs/npm-kubo const goenv = require("./go-platform"); const gunzip = require("gunzip-maybe"); const got = require("got").default; const path = require("path"); const tarFS = require("tar-fs"); const unzip = require("unzip-stream"); const pkgConf = require("pkg-conf"); const cachedir = require("cachedir"); const fs = require("fs"); const hasha = require("hasha"); // Version of odo to install. This is known to be working with this plugin. // Can be overridden by clients either via the BACKSTAGE_ODO_PLUGIN__ODO_VERSION environment variable // or via the 'odo.version' field in their 'package.json' file. const ODO_VERSION = "3.15.0"; const ODO_DIST_URL = "https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/odo"; // Map of all architectures that can be donwloaded on the odo distribution URL. const SUPPORTED_ARCHITECTURES_BY_PLATFORM = new Map( Object.entries({ darwin: ["amd64", "arm64"], linux: ["amd64", "arm64", "ppc64le", "s390x"], windows: ["amd64"], }) ); /** * This avoids an expensive donwload if file is already in cache. * * @param {string} url * @param {string} platform * @param {string} arch * @param {string} version */ async function cachingFetchAndVerify(url, platform, arch, version) { const parentCacheDir = cachedir("odo"); const cacheDir = path.join(parentCacheDir, version); const filename = url.split("/").pop(); if (!filename) { throw new Error(`Invalid URL: ${url}`); } const cachedFilePath = path.join(cacheDir, filename); const cachedHashPath = `${cachedFilePath}.sha256`; if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } if (version === "latest" || !fs.existsSync(cachedFilePath)) { console.info(`Downloading ${url} to ${cacheDir}`); // download file fs.writeFileSync(cachedFilePath, await got(url).buffer(), { flag: fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY, }); console.info(`Downloaded ${url}`); // ..and checksum console.info(`Downloading ${filename}.sha256`); fs.writeFileSync(cachedHashPath, await got(`${url}.sha256`).buffer(), { flag: fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY, }); console.info(`Downloaded ${filename}.sha256`); } else { console.info(`Found ${cachedFilePath}`); } console.info(`Verifying ${filename}.sha256`); const digest = Buffer.alloc(64); const fd = fs.openSync(cachedHashPath, "r"); fs.readSync(fd, digest, 0, digest.length, 0); fs.closeSync(fd); const expectedSha = digest.toString("utf8"); const calculatedSha = await hasha.fromFile(cachedFilePath, { encoding: "hex", algorithm: "sha256", }); if (calculatedSha !== expectedSha) { console.log(`calculatedSha: ${calculatedSha.length}`); console.log(`expectedSha: ${expectedSha.length}`); throw new Error( `sha256 mismatch for file ${cachedFilePath}. Expected '${expectedSha}', but calculated '${calculatedSha}'. Maybe the file downloaded was incomplete? Try to delete the file to force a re-download: ${cachedFilePath}` ); } console.log(`OK (${expectedSha})`); const data = fs.createReadStream(cachedFilePath); await unpack(url, parentCacheDir, data); console.info(`Unpacked into ${parentCacheDir}`); // Rename file if needed let resultingFileName = "odo"; switch (platform) { case "windows": resultingFileName = "odo.exe"; fs.renameSync(path.join(parentCacheDir, `odo-${platform}-${arch}.exe`), path.join(parentCacheDir, resultingFileName)); break; case "darwin": fs.renameSync(path.join(parentCacheDir, `odo-${platform}-${arch}`), path.join(parentCacheDir, resultingFileName)); break; default: break; } const p = path.join(parentCacheDir, resultingFileName); console.log(`odo binary (${version}) available at '${p}'`); return p; } /** * @param {string} url * @param {string} installPath * @param {import('stream').Readable} stream */ function unpack(url, installPath, stream) { return new Promise((resolve, reject) => { if (url.endsWith(".zip")) { return stream.pipe( unzip .Extract({ path: installPath }) .on("close", resolve) .on("error", reject) ); } return stream .pipe(gunzip()) .pipe( tarFS.extract(installPath).on("finish", resolve).on("error", reject) ); }); } /** * @param {object} options * @param {string} options.version * @param {string} options.platform * @param {string} options.arch */ async function download({ version, platform, arch }) { let versionToDl = version; if (versionToDl !== "latest" && !versionToDl.startsWith("v")) { versionToDl = `v${version}`; } const url = `${ODO_DIST_URL}/${versionToDl}/odo-${platform}-${arch}.${ platform === "windows" ? "exe.zip" : "tar.gz" }`; return await cachingFetchAndVerify(url, platform, arch, version); } /** * @param {string} [platform] * @param {string} [arch] */ function enforcePlatformAndArch(platform, arch) { if (!SUPPORTED_ARCHITECTURES_BY_PLATFORM.has(platform)) { throw new Error( `No binary available for platform: ${platform}. Supported platforms: ${Array.from( SUPPORTED_ARCHITECTURES_BY_PLATFORM.keys() ).join(", ")}` ); } const archs = SUPPORTED_ARCHITECTURES_BY_PLATFORM.get(platform); if (!archs?.includes(arch)) { throw new Error( `No binary available for platform/arch: ${platform}/${arch}. Supported architectures for ${platform}: ${archs.join( ", " )}` ); } } /** * @param {string} [version] * @param {string} [platform] * @param {string} [arch] */ function buildArguments(version, platform, arch) { const conf = pkgConf.sync("odo", { cwd: process.cwd(), defaults: { version: ODO_VERSION, distUrl: ODO_DIST_URL, }, }); return { version: process.env.BACKSTAGE_ODO_PLUGIN__ODO_VERSION || version || conf.version, platform: process.env.BACKSTAGE_ODO_PLUGIN__TARGET_OS || platform || goenv.GOOS, arch: process.env.BACKSTAGE_ODO_PLUGIN__TARGET_ARCH || arch || goenv.GOARCH, }; } /** * @param {string} [version] * @param {string} [platform] * @param {string} [arch] */ module.exports = async (version, platform, arch) => { const args = buildArguments(version, platform, arch); enforcePlatformAndArch(args.platform, args.arch); return await download(args); };