mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix opening of release details when release contains resources that do not have namespaces at all
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
2120915bf3
commit
22216f3d45
@ -11496,7 +11496,21 @@ exports[`installing helm chart from new tab given tab for installing chart was n
|
||||
<div
|
||||
class="drawer-title-text flex gaps align-center"
|
||||
>
|
||||
|
||||
some-release
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="content_copy"
|
||||
>
|
||||
content_copy
|
||||
</span>
|
||||
</i>
|
||||
<div>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
|
||||
@ -29,6 +29,8 @@ import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/h
|
||||
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import callForHelmReleasesInjectable from "../../../renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import callForHelmReleaseDetailsInjectable from "../../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
|
||||
describe("installing helm chart from new tab", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
@ -50,6 +52,9 @@ describe("installing helm chart from new tab", () => {
|
||||
callForCreateHelmReleaseMock = asyncFn();
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
windowDi.override(callForHelmReleasesInjectable, () => async () => []);
|
||||
windowDi.override(callForHelmReleaseDetailsInjectable, () => () => new Promise(() => {}));
|
||||
|
||||
windowDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
|
||||
@ -8,14 +8,16 @@ import helmBinaryPathInjectable from "../helm-binary-path.injectable";
|
||||
import type { AsyncResult } from "../../../common/utils/async-result";
|
||||
import { getErrorMessage } from "../../../common/utils/get-error-message";
|
||||
|
||||
export type ExecHelm = (...args: string[]) => Promise<AsyncResult<string>>;
|
||||
|
||||
const execHelmInjectable = getInjectable({
|
||||
id: "exec-helm",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): ExecHelm => {
|
||||
const execFile = di.inject(execFileInjectable);
|
||||
const helmBinaryPath = di.inject(helmBinaryPathInjectable);
|
||||
|
||||
return async (...args: string[]): Promise<AsyncResult<string>> => {
|
||||
return async (...args) => {
|
||||
try {
|
||||
const response = await execFile(helmBinaryPath, args);
|
||||
|
||||
|
||||
@ -7,10 +7,8 @@ import tempy from "tempy";
|
||||
import fse from "fs-extra";
|
||||
import * as yaml from "js-yaml";
|
||||
import { toCamelCase } from "../../common/utils/camelCase";
|
||||
import { execFile } from "child_process";
|
||||
import { execHelm } from "./exec";
|
||||
import assert from "assert";
|
||||
import type { JsonObject, JsonValue } from "type-fest";
|
||||
import type { JsonValue } from "type-fest";
|
||||
import { isObject, json } from "../../common/utils";
|
||||
|
||||
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
|
||||
@ -77,55 +75,6 @@ export async function installChart(chart: string, values: JsonValue, name: strin
|
||||
}
|
||||
}
|
||||
|
||||
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(valuesFilePath, yaml.dump(values));
|
||||
|
||||
const args = [
|
||||
"upgrade",
|
||||
name,
|
||||
chart,
|
||||
"--version", version,
|
||||
"--values", valuesFilePath,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
];
|
||||
|
||||
try {
|
||||
const output = await execHelm(args);
|
||||
|
||||
return {
|
||||
log: output,
|
||||
release: await getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||
};
|
||||
} finally {
|
||||
await fse.unlink(valuesFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
||||
const args = [
|
||||
"status",
|
||||
name,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
"--output", "json",
|
||||
];
|
||||
|
||||
const release = json.parse(await execHelm(args, {
|
||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||
}));
|
||||
|
||||
if (!isObject(release) || Array.isArray(release)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
|
||||
return execHelm([
|
||||
"delete",
|
||||
@ -180,62 +129,3 @@ export async function rollback(name: string, namespace: string, revision: number
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
]);
|
||||
}
|
||||
|
||||
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
||||
const helmArgs = [
|
||||
"get",
|
||||
"manifest",
|
||||
name,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
];
|
||||
const kubectlArgs = [
|
||||
"get",
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
"-f", "-",
|
||||
"--output", "json",
|
||||
// Temporary workaround for https://github.com/lensapp/lens/issues/6031
|
||||
// and other potential issues where resources can't be found. Showing
|
||||
// no resources is better than the app hard-locking, and at least
|
||||
// the helm metadata is shown.
|
||||
"--ignore-not-found",
|
||||
];
|
||||
|
||||
try {
|
||||
const helmOutput = await execHelm(helmArgs);
|
||||
|
||||
return new Promise<JsonObject[]>((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const kubectl = execFile(kubectlPath, kubectlArgs);
|
||||
|
||||
kubectl
|
||||
.on("exit", (code, signal) => {
|
||||
if (typeof code === "number") {
|
||||
if (code === 0) {
|
||||
if (stdout === "") {
|
||||
resolve([]);
|
||||
} else {
|
||||
const output = json.parse(stdout) as { items: JsonObject[] };
|
||||
|
||||
resolve(output.items);
|
||||
}
|
||||
} else {
|
||||
reject(stderr);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Kubectl exited with signal ${signal}`));
|
||||
}
|
||||
})
|
||||
.on("error", reject);
|
||||
|
||||
assert(kubectl.stderr && kubectl.stdout && kubectl.stdin, "For some reason the IO streams are undefined");
|
||||
|
||||
kubectl.stderr.on("data", output => stderr += output);
|
||||
kubectl.stdout.on("data", output => stdout += output);
|
||||
kubectl.stdin.end(helmOutput);
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { AsyncResult } from "../../../../../common/utils/async-result";
|
||||
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export interface HelmResourceManifest {
|
||||
metadata: {
|
||||
namespace: string;
|
||||
};
|
||||
}
|
||||
|
||||
const callForHelmManifestInjectable = getInjectable({
|
||||
id: "call-for-helm-manifest",
|
||||
|
||||
instantiate: (di) => {
|
||||
const execHelm = di.inject(execHelmInjectable);
|
||||
|
||||
return async (
|
||||
name: string,
|
||||
namespace: string,
|
||||
kubeconfigPath: string,
|
||||
): Promise<AsyncResult<HelmResourceManifest[]>> => {
|
||||
const result = await execHelm(
|
||||
"get",
|
||||
"manifest",
|
||||
name,
|
||||
"--namespace",
|
||||
namespace,
|
||||
"--kubeconfig",
|
||||
kubeconfigPath,
|
||||
);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
return { callWasSuccessful: false, error: result.error };
|
||||
}
|
||||
|
||||
return {
|
||||
callWasSuccessful: true,
|
||||
response: yaml
|
||||
.loadAll(result.response)
|
||||
.filter((manifest) => !!manifest) as HelmResourceManifest[],
|
||||
};
|
||||
};
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export default callForHelmManifestInjectable;
|
||||
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import { json } from "../../../../../common/utils";
|
||||
import yaml from "js-yaml";
|
||||
import execFileWithInputInjectable from "./exec-file-with-input/exec-file-with-input.injectable";
|
||||
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
|
||||
import { map } from "lodash/fp";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import type { HelmResourceManifest } from "../call-for-helm-manifest/call-for-helm-manifest.injectable";
|
||||
|
||||
export type CallForKubeResourcesByManifest = (
|
||||
namespace: string,
|
||||
kubeconfigPath: string,
|
||||
kubectlPath: string,
|
||||
resourceManifests: HelmResourceManifest[]
|
||||
) => Promise<JsonObject[]>;
|
||||
|
||||
const callForKubeResourcesByManifestInjectable = getInjectable({
|
||||
id: "call-for-kube-resources-by-manifest",
|
||||
|
||||
instantiate: (di): CallForKubeResourcesByManifest => {
|
||||
const execFileWithInput = di.inject(execFileWithInputInjectable);
|
||||
|
||||
return async (
|
||||
namespace,
|
||||
kubeconfigPath,
|
||||
kubectlPath,
|
||||
resourceManifests,
|
||||
) => {
|
||||
const input = pipeline(
|
||||
resourceManifests,
|
||||
map((manifest) => yaml.dump(manifest)),
|
||||
wideJoin("---\n"),
|
||||
);
|
||||
|
||||
const result = await execFileWithInput({
|
||||
filePath: kubectlPath,
|
||||
input,
|
||||
|
||||
commandArguments: [
|
||||
"get",
|
||||
"--kubeconfig",
|
||||
kubeconfigPath,
|
||||
"-f",
|
||||
"-",
|
||||
"--namespace",
|
||||
namespace,
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
});
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
const errorMessage = getErrorMessage(result.error);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const output = json.parse(result.response) as { items: JsonObject[] };
|
||||
|
||||
return output.items;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default callForKubeResourcesByManifestInjectable;
|
||||
|
||||
const wideJoin = (joiner: string) => (items: string[]) =>
|
||||
`${joiner}${items.join(joiner)}${joiner}`;
|
||||
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
|
||||
import execFileWithInputInjectable from "./exec-file-with-input.injectable";
|
||||
|
||||
export default getGlobalOverride(execFileWithInputInjectable, () => () => {
|
||||
throw new Error(
|
||||
"Tried to call exec file with input without explicit override",
|
||||
);
|
||||
});
|
||||
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
|
||||
import { isNumber } from "../../../../../../common/utils";
|
||||
import assert from "assert";
|
||||
import type { ChildProcess } from "child_process";
|
||||
|
||||
export type ExecFileWithInput = (options: {
|
||||
filePath: string;
|
||||
commandArguments: string[];
|
||||
input: string;
|
||||
}) => Promise<AsyncResult<string, unknown>>;
|
||||
|
||||
const execFileWithInputInjectable = getInjectable({
|
||||
id: "exec-file-with-input",
|
||||
|
||||
instantiate: (di): ExecFileWithInput => {
|
||||
const execFile = di.inject(nonPromiseExecFileInjectable);
|
||||
|
||||
return async ({ filePath, commandArguments, input }) =>
|
||||
new Promise((resolve) => {
|
||||
let execution: ChildProcess;
|
||||
|
||||
try {
|
||||
execution = execFile(filePath, commandArguments);
|
||||
} catch (e) {
|
||||
resolve({ callWasSuccessful: false, error: e });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert(execution.stdout, "stdout is not defined");
|
||||
assert(execution.stderr, "stderr is not defined");
|
||||
assert(execution.stdin, "stdin is not defined");
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
execution.stdout.on("data", (data) => {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
execution.stderr.on("data", (data) => {
|
||||
stderr += data;
|
||||
});
|
||||
|
||||
execution.on("error", (error) =>
|
||||
resolve({ callWasSuccessful: false, error }),
|
||||
);
|
||||
|
||||
execution.on("exit", (code, signal) => {
|
||||
if (!isNumber(code)) {
|
||||
resolve({
|
||||
callWasSuccessful: false,
|
||||
error: "Exited without exit code",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
resolve({
|
||||
callWasSuccessful: false,
|
||||
error: stderr ? stderr : `Failed with error: ${signal}`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ callWasSuccessful: true, response: stdout });
|
||||
});
|
||||
|
||||
execution.stdin.end(input);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default execFileWithInputInjectable;
|
||||
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getDiForUnitTesting } from "../../../../../getDiForUnitTesting";
|
||||
import type { ExecFileWithInput } from "./exec-file-with-input.injectable";
|
||||
import execFileWithInputInjectable from "./exec-file-with-input.injectable";
|
||||
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
|
||||
import { getPromiseStatus } from "../../../../../../common/test-utils/get-promise-status";
|
||||
import EventEmitter from "events";
|
||||
|
||||
describe("exec-file-with-input", () => {
|
||||
let execFileWithInput: ExecFileWithInput;
|
||||
let execFileMock: jest.Mock;
|
||||
|
||||
let executionStub: EventEmitter & {
|
||||
stdin: { end: jest.Mock };
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.unoverride(execFileWithInputInjectable);
|
||||
|
||||
executionStub = new EventEmitter() as any;
|
||||
executionStub.stdin = { end: jest.fn() };
|
||||
executionStub.stdout = new EventEmitter();
|
||||
executionStub.stderr = new EventEmitter();
|
||||
|
||||
execFileMock = jest.fn(() => executionStub);
|
||||
|
||||
di.override(nonPromiseExecFileInjectable, () => execFileMock as any);
|
||||
|
||||
execFileWithInput = di.inject(execFileWithInputInjectable);
|
||||
});
|
||||
|
||||
it("given call, when throws synchronously, resolves with failure", async () => {
|
||||
execFileMock.mockImplementation(() => {
|
||||
throw new Error("some-error");
|
||||
});
|
||||
|
||||
const actual = await execFileWithInput({
|
||||
filePath: "./irrelevant",
|
||||
commandArguments: ["irrelevant"],
|
||||
input: "irrelevant",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let actualPromise: Promise<AsyncResult<string, unknown>>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = execFileWithInput({
|
||||
filePath: "./some-file-path",
|
||||
commandArguments: ["some-arg", "some-other-arg"],
|
||||
input: "some-input",
|
||||
});
|
||||
});
|
||||
|
||||
it("calls for file with arguments", () => {
|
||||
expect(execFileMock).toHaveBeenCalledWith("./some-file-path", [
|
||||
"some-arg",
|
||||
"some-other-arg",
|
||||
]);
|
||||
});
|
||||
|
||||
it("calls with input", () => {
|
||||
expect(executionStub.stdin.end).toHaveBeenCalledWith("some-input");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when stdout receives data", () => {
|
||||
beforeEach(() => {
|
||||
executionStub.stdout.emit("data", "some-data");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when stdout receives more data", () => {
|
||||
beforeEach(() => {
|
||||
executionStub.stdout.emit("data", "some-other-data");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when execution exits with success, resolves with result", async () => {
|
||||
executionStub.emit("exit", 0);
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: true,
|
||||
response: "some-datasome-other-data",
|
||||
});
|
||||
});
|
||||
|
||||
it("when execution exits without exit code, resolves with failure", async () => {
|
||||
executionStub.emit("exit");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: "Exited without exit code",
|
||||
});
|
||||
});
|
||||
|
||||
it("when execution exits with failure, resolves with failure", async () => {
|
||||
executionStub.emit("exit", 42, "some-signal");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: "Failed with error: some-signal",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when stderr receives data", () => {
|
||||
beforeEach(() => {
|
||||
executionStub.stderr.emit("data", "some-error");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when stderr receives more data", () => {
|
||||
beforeEach(() => {
|
||||
executionStub.stderr.emit("data", "some-other-error");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when execution exits with success, resolves with result", async () => {
|
||||
executionStub.emit("exit", 0);
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: true,
|
||||
response: "some-datasome-other-data",
|
||||
});
|
||||
});
|
||||
|
||||
it("when execution exits without exit code, resolves with failure", async () => {
|
||||
executionStub.emit("exit");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: "Exited without exit code",
|
||||
});
|
||||
});
|
||||
|
||||
it("when execution exits with failure, resolves with errors", async () => {
|
||||
executionStub.emit("exit", 42, "irrelevant");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: "some-errorsome-other-error",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("when execution receives error, resolves with error", async () => {
|
||||
executionStub.emit("error", new Error("some-error"));
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: false,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 } from "@ogre-tools/injectable";
|
||||
import { execFile } from "child_process";
|
||||
|
||||
const nonPromiseExecFileInjectable = getInjectable({
|
||||
id: "non-promise-exec-file",
|
||||
instantiate: () => execFile,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default nonPromiseExecFileInjectable;
|
||||
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import callForKubeResourcesByManifestInjectable from "./call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable";
|
||||
import { groupBy, map } from "lodash/fp";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
|
||||
|
||||
export type GetHelmReleaseResources = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
kubeconfigPath: string,
|
||||
kubectlPath: string
|
||||
) => Promise<JsonObject[]>;
|
||||
|
||||
const getHelmReleaseResourcesInjectable = getInjectable({
|
||||
id: "get-helm-release-resources",
|
||||
|
||||
instantiate: (di): GetHelmReleaseResources => {
|
||||
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
|
||||
const callForKubeResourcesByManifest = di.inject(callForKubeResourcesByManifestInjectable);
|
||||
|
||||
return async (name, namespace, kubeconfigPath, kubectlPath) => {
|
||||
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const results = await pipeline(
|
||||
result.response,
|
||||
|
||||
groupBy((item) => item.metadata.namespace || namespace),
|
||||
|
||||
(x) => Object.entries(x),
|
||||
|
||||
map(([namespace, manifest]) =>
|
||||
callForKubeResourcesByManifest(namespace, kubeconfigPath, kubectlPath, manifest),
|
||||
),
|
||||
|
||||
promises => Promise.all(promises),
|
||||
);
|
||||
|
||||
return results.flat(1);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default getHelmReleaseResourcesInjectable;
|
||||
@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { GetHelmReleaseResources } from "./get-helm-release-resources.injectable";
|
||||
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources.injectable";
|
||||
import type { ExecHelm } from "../../exec-helm/exec-helm.injectable";
|
||||
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import type { ExecFileWithInput } from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
|
||||
import execFileWithInputInjectable from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
|
||||
|
||||
describe("get helm release resources", () => {
|
||||
let getHelmReleaseResources: GetHelmReleaseResources;
|
||||
let execHelmMock: AsyncFnMock<ExecHelm>;
|
||||
let execFileWithStreamInputMock: AsyncFnMock<ExecFileWithInput>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
execHelmMock = asyncFn();
|
||||
execFileWithStreamInputMock = asyncFn();
|
||||
|
||||
di.override(execHelmInjectable, () => execHelmMock);
|
||||
|
||||
di.override(
|
||||
execFileWithInputInjectable,
|
||||
() => execFileWithStreamInputMock,
|
||||
);
|
||||
|
||||
getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let actualPromise: Promise<JsonObject[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = getHelmReleaseResources(
|
||||
"some-release",
|
||||
"some-namespace",
|
||||
"/some-kubeconfig-path",
|
||||
"/some-kubectl-path",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls for release manifest", () => {
|
||||
expect(execHelmMock).toHaveBeenCalledWith(
|
||||
"get", "manifest", "some-release", "--namespace", "some-namespace", "--kubeconfig", "/some-kubeconfig-path",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call for resources yet", () => {
|
||||
expect(execFileWithStreamInputMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when call for manifest resolves without resources, resolves without resources", async () => {
|
||||
await execHelmMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: "",
|
||||
});
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
describe("when call for manifest resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await execHelmMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: `---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-with-same-namespace
|
||||
namespace: some-namespace
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-without-namespace
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-with-different-namespace
|
||||
namespace: some-other-namespace
|
||||
---
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls for resources from each namespace separately using the manifest as input", () => {
|
||||
expect(execFileWithStreamInputMock.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
filePath: "/some-kubectl-path",
|
||||
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-namespace", "--output", "json"],
|
||||
input: `---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-with-same-namespace
|
||||
namespace: some-namespace
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-without-namespace
|
||||
---
|
||||
`,
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
filePath: "/some-kubectl-path",
|
||||
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-other-namespace", "--output", "json"],
|
||||
input: `---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-with-different-namespace
|
||||
namespace: some-other-namespace
|
||||
---
|
||||
`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("when all calls for resources resolve, resolves with combined result", async () => {
|
||||
await execFileWithStreamInputMock.resolveSpecific(
|
||||
([{ commandArguments }]) =>
|
||||
commandArguments.includes("some-namespace"),
|
||||
{
|
||||
callWasSuccessful: true,
|
||||
|
||||
response: JSON.stringify({
|
||||
items: [{ some: "item" }],
|
||||
|
||||
kind: "List",
|
||||
|
||||
metadata: {
|
||||
resourceVersion: "",
|
||||
selfLink: "",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await execFileWithStreamInputMock.resolveSpecific(
|
||||
([{ commandArguments }]) =>
|
||||
commandArguments.includes("some-other-namespace"),
|
||||
{
|
||||
callWasSuccessful: true,
|
||||
|
||||
response: JSON.stringify({
|
||||
items: [{ some: "other-item" }],
|
||||
|
||||
kind: "List",
|
||||
|
||||
metadata: {
|
||||
resourceVersion: "",
|
||||
selfLink: "",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual([{ some: "item" }, { some: "other-item" }]);
|
||||
});
|
||||
|
||||
it("given some call fails, when all calls have finished, rejects with failure", async () => {
|
||||
await execFileWithStreamInputMock.resolveSpecific(
|
||||
([{ commandArguments }]) =>
|
||||
commandArguments.includes("some-namespace"),
|
||||
|
||||
{
|
||||
callWasSuccessful: true,
|
||||
|
||||
response: JSON.stringify({
|
||||
items: [{ some: "item" }],
|
||||
|
||||
kind: "List",
|
||||
|
||||
metadata: {
|
||||
resourceVersion: "",
|
||||
selfLink: "",
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
execFileWithStreamInputMock.resolveSpecific(
|
||||
([{ commandArguments }]) =>
|
||||
commandArguments.includes("some-other-namespace"),
|
||||
|
||||
{
|
||||
callWasSuccessful: false,
|
||||
error: "some-error",
|
||||
},
|
||||
);
|
||||
|
||||
return expect(actualPromise).rejects.toEqual(expect.any(Error));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -4,14 +4,17 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { getRelease } from "../helm-release-manager";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { isObject, json } from "../../../common/utils";
|
||||
import { execHelm } from "../exec";
|
||||
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable";
|
||||
|
||||
const getHelmReleaseInjectable = getInjectable({
|
||||
id: "get-helm-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
|
||||
|
||||
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
||||
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
||||
@ -20,7 +23,35 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
|
||||
logger.debug("Fetch release");
|
||||
|
||||
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
|
||||
const args = [
|
||||
"status",
|
||||
releaseName,
|
||||
"--namespace",
|
||||
namespace,
|
||||
"--kubeconfig",
|
||||
kubeconfigPath,
|
||||
"--output",
|
||||
"json",
|
||||
];
|
||||
|
||||
const release = json.parse(
|
||||
await execHelm(args, {
|
||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isObject(release) || Array.isArray(release)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
release.resources = await getHelmReleaseResources(
|
||||
releaseName,
|
||||
namespace,
|
||||
kubeconfigPath,
|
||||
kubectlPath,
|
||||
);
|
||||
|
||||
return release;
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -4,9 +4,13 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { upgradeRelease } from "../helm-release-manager";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import { execHelm } from "../exec";
|
||||
import tempy from "tempy";
|
||||
import fse from "fs-extra";
|
||||
import yaml from "js-yaml";
|
||||
import getHelmReleaseInjectable from "./get-helm-release.injectable";
|
||||
|
||||
export interface UpdateChartArgs {
|
||||
chart: string;
|
||||
@ -19,23 +23,37 @@ const updateHelmReleaseInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const getHelmRelease = di.inject(getHelmReleaseInjectable);
|
||||
|
||||
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const kubectl = await cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
|
||||
logger.debug("Upgrade release");
|
||||
|
||||
return upgradeRelease(
|
||||
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
||||
|
||||
await fse.writeFile(valuesFilePath, yaml.dump(data.values));
|
||||
|
||||
const args = [
|
||||
"upgrade",
|
||||
releaseName,
|
||||
data.chart,
|
||||
data.values,
|
||||
namespace,
|
||||
data.version,
|
||||
proxyKubeconfig,
|
||||
kubectlPath,
|
||||
);
|
||||
"--version", data.version,
|
||||
"--values", valuesFilePath,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", proxyKubeconfig,
|
||||
];
|
||||
|
||||
try {
|
||||
const output = await execHelm(args);
|
||||
|
||||
return {
|
||||
log: output,
|
||||
release: await getHelmRelease(cluster, releaseName, namespace),
|
||||
};
|
||||
} finally {
|
||||
await fse.unlink(valuesFilePath);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@ -43,3 +61,4 @@ const updateHelmReleaseInjectable = getInjectable({
|
||||
});
|
||||
|
||||
export default updateHelmReleaseInjectable;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user