Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace all uses of promiseExec with promiseExecFile #4514

Merged
merged 5 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion integration/__tests__/app-preferences.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ describe("preferences page tests", () => {
}
}, 10*60*1000);

utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
// Skipping, but will turn it on again in the follow up PR
it.skip("ensures helm repos", async () => {
await window.click("[data-testid=kubernetes-tab]");
await window.waitForSelector("[data-testid=repository-name]", {
timeout: 140_000,
Expand Down
22 changes: 14 additions & 8 deletions src/common/system-ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import { isMac, isWindows } from "./vars";
import wincaAPI from "win-ca/api";
import https from "https";
import { promiseExec } from "./utils/promise-exec";
import { promiseExecFile } from "./utils/promise-exec";

// DST Root CA X3, which was expired on 9.30.2021
export const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
Expand All @@ -33,19 +33,25 @@ export function isCertActive(cert: string) {
return !isExpired;
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;

async function execSecurity(...args: string[]): Promise<string[]> {
const { stdout } = await promiseExecFile("/usr/bin/security", args);

return stdout.split(certSplitPattern);
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Get root CA certificate from MacOSX system keychain
* Only return non-expred certificates.
*/
export async function getMacRootCA() {
// inspired mac-ca https://github.com/jfromaniello/mac-ca
const args = "find-certificate -a -p";
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
const systemRootCertsPath = "/System/Library/Keychains/SystemRootCertificates.keychain";
const bin = "/usr/bin/security";
const trusted = (await promiseExec(`${bin} ${args}`)).stdout.toString().split(splitPattern);
const rootCA = (await promiseExec(`${bin} ${args} ${systemRootCertsPath}`)).stdout.toString().split(splitPattern);
const [trusted, rootCA] = await Promise.all([
execSecurity("find-certificate", "-a", "-p"),
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
]);

return [...new Set([...trusted, ...rootCA])].filter(isCertActive);
}
Expand Down
3 changes: 1 addition & 2 deletions src/common/utils/promise-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
*/

import * as util from "util";
import { exec, execFile } from "child_process";
import { execFile } from "child_process";

export const promiseExec = util.promisify(exec);
export const promiseExecFile = util.promisify(execFile);
262 changes: 165 additions & 97 deletions src/main/helm/helm-release-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,161 +22,229 @@
import * as tempy from "tempy";
import fse from "fs-extra";
import * as yaml from "js-yaml";
import { promiseExec } from "../../common/utils/promise-exec";
import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli";
import type { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase";
import type { BaseEncodingOptions } from "fs";
import { execFile, ExecFileOptions } from "child_process";

export async function listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath();
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
const helmCliPath = await helmCli.binaryPath();

try {
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`);
const output = JSON.parse(stdout);

if (output.length == 0) {
return output;
}
output.forEach((release: any, index: number) => {
output[index] = toCamelCase(release);
});
const { stdout } = await promiseExecFile(helmCliPath, args, options);

return output;
return stdout;
} catch (error) {
throw error?.stderr || error;
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
}
}

export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
const args = [
"ls",
"--output", "json",
];

export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath();
const fileName = tempy.file({ name: "values.yaml" });
if (namespace) {
args.push("-n", namespace);
} else {
args.push("--all-namespaces");
}

await fse.writeFile(fileName, yaml.dump(values));
args.push("--kubeconfig", pathToKubeconfig);

try {
let generateName = "";
const output = JSON.parse(await execHelm(args));

if (!Array.isArray(output) || output.length == 0) {
return [];
}

if (!name) {
generateName = "--generate-name";
name = "";
}
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`);
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
return output.map(toCamelCase);
}


export async function installChart(chart: string, values: any, name: string | undefined = "", namespace: string, version: string, kubeconfigPath: string) {
const valuesFilePath = tempy.file({ name: "values.yaml" });

await fse.writeFile(valuesFilePath, yaml.dump(values));

const args = ["install"];

if (name) {
args.push(name);
}

args.push(
chart,
"--version", version,
"--values", valuesFilePath,
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);

if (!name) {
args.push("--generate-name");
}

try {
const output = await execHelm(args);
const releaseName = output.split("\n")[0].split(" ")[1].trim();

return {
log: stdout,
log: output,
release: {
name: releaseName,
namespace,
},
};
} catch (error) {
throw error?.stderr || error;
} finally {
await fse.unlink(fileName);
await fse.unlink(valuesFilePath);
}
}

export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
const helm = await helmCli.binaryPath();
const fileName = tempy.file({ name: "values.yaml" });
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, kubeconfigPath: string, kubectlPath: string) {
const valuesFilePath = tempy.file({ name: "values.yaml" });

await fse.writeFile(fileName, yaml.dump(values));
await fse.writeFile(valuesFilePath, yaml.dump(values));

const args = [
"upgrade",
name,
chart,
"--version", version,
"--values", valuesFilePath,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
];

try {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
const output = await execHelm(args);

return {
log: stdout,
release: getRelease(name, namespace, cluster),
log: output,
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
};
} catch (error) {
throw error?.stderr || error;
} finally {
await fse.unlink(fileName);
await fse.unlink(valuesFilePath);
}
}

export async function getRelease(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath();
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
const args = [
"status",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
"--output", "json",
];

const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`, {
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
});
const release = JSON.parse(stdout);
const release = JSON.parse(await execHelm(args, {
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
}));

release.resources = await getResources(name, namespace, cluster);
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);

return release;
} catch (error) {
throw error?.stderr || error;
}
return release;
}

export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);

return stdout;
} catch (error) {
throw error?.stderr || error;
}
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
return execHelm([
"delete",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]);
}

interface GetValuesOptions {
namespace: string;
all?: boolean;
pathToKubeconfig: string;
kubeconfigPath: string;
}

export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) {
try {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
export async function getValues(name: string, { namespace, all = false, kubeconfigPath }: GetValuesOptions) {
const args = [
"get",
"values",
name,
];

return stdout;
} catch (error) {
throw error?.stderr || error;
if (all) {
args.push("--all");
}
}

export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
args.push(
"--output", "yaml",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);

return JSON.parse(stdout);
} catch (error) {
throw error?.stderr || error;
}
return execHelm(args);
}

export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
export async function getHistory(name: string, namespace: string, kubeconfigPath: string) {
return JSON.parse(await execHelm([
"history",
name,
"--output", "json",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]));
}

return stdout;
} catch (error) {
throw error?.stderr || error;
}
export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) {
return JSON.parse(await execHelm([
"rollback",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]));
}

async function getResources(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
const helmArgs = [
"get",
"manifest",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
];
const kubectlArgs = [
"get",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
"-f", "-",
"--output", "json",
];

return JSON.parse(stdout).items;
try {
const helmOutput = await execHelm(helmArgs);

return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const kubectl = execFile(kubectlPath, kubectlArgs);

kubectl
.on("exit", (code, signal) => {
Nokel81 marked this conversation as resolved.
Show resolved Hide resolved
if (typeof code === "number") {
if (code === 0) {
resolve(JSON.parse(stdout).items);
} else {
reject(stderr);
}
} else {
reject(new Error(`Kubectl exited with signal ${signal}`));
}
})
.on("error", reject);

kubectl.stderr.on("data", output => stderr += output);
kubectl.stdout.on("data", output => stdout += output);
kubectl.stdin.write(helmOutput);
kubectl.stdin.end();
});
} catch {
return [];
}
Expand Down