diff --git a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
index dee4f06b14..15c98bdca1 100644
--- a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
+++ b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
@@ -11496,7 +11496,21 @@ exports[`installing helm chart from new tab given tab for installing chart was n
-
+ some-release
+
+
+ content_copy
+
+
+
+ Copy
+
{
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",
diff --git a/src/main/helm/exec-helm/exec-helm.injectable.ts b/src/main/helm/exec-helm/exec-helm.injectable.ts
index a97953706d..97062d22eb 100644
--- a/src/main/helm/exec-helm/exec-helm.injectable.ts
+++ b/src/main/helm/exec-helm/exec-helm.injectable.ts
@@ -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>;
+
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> => {
+ return async (...args) => {
try {
const response = await execFile(helmBinaryPath, args);
diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts
index f0907dc3c2..29bbbdd830 100644
--- a/src/main/helm/helm-release-manager.ts
+++ b/src/main/helm/helm-release-manager.ts
@@ -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[]> {
@@ -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((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 [];
- }
-}
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts
new file mode 100644
index 0000000000..e23966d00d
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts
@@ -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> => {
+ 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;
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable.ts
new file mode 100644
index 0000000000..c530c98385
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable.ts
@@ -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;
+
+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}`;
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.global-override-for-injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.global-override-for-injectable.ts
new file mode 100644
index 0000000000..70b4771468
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.global-override-for-injectable.ts
@@ -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",
+ );
+});
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable.ts
new file mode 100644
index 0000000000..fedcf8ccfe
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable.ts
@@ -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>;
+
+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;
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.test.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.test.ts
new file mode 100644
index 0000000000..7a607da5b8
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.test.ts
@@ -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>;
+
+ 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),
+ });
+ });
+ });
+});
diff --git a/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/non-promise-exec-file.injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/non-promise-exec-file.injectable.ts
new file mode 100644
index 0000000000..034f5e3055
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/call-for-kube-resources-by-manifest/exec-file-with-input/non-promise-exec-file.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 } from "@ogre-tools/injectable";
+import { execFile } from "child_process";
+
+const nonPromiseExecFileInjectable = getInjectable({
+ id: "non-promise-exec-file",
+ instantiate: () => execFile,
+ causesSideEffects: true,
+});
+
+export default nonPromiseExecFileInjectable;
diff --git a/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts b/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts
new file mode 100644
index 0000000000..025aab6b4d
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts
@@ -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;
+
+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;
diff --git a/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.test.ts b/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.test.ts
new file mode 100644
index 0000000000..2ee661a46d
--- /dev/null
+++ b/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.test.ts
@@ -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;
+ let execFileWithStreamInputMock: AsyncFnMock;
+
+ 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;
+
+ 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));
+ });
+ });
+ });
+});
diff --git a/src/main/helm/helm-service/get-helm-release.injectable.ts b/src/main/helm/helm-service/get-helm-release.injectable.ts
index 130d94af52..79177415a2 100644
--- a/src/main/helm/helm-service/get-helm-release.injectable.ts
+++ b/src/main/helm/helm-service/get-helm-release.injectable.ts
@@ -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;
};
},
diff --git a/src/main/helm/helm-service/update-helm-release.injectable.ts b/src/main/helm/helm-service/update-helm-release.injectable.ts
index 96e0e0050b..e07269ce6a 100644
--- a/src/main/helm/helm-service/update-helm-release.injectable.ts
+++ b/src/main/helm/helm-service/update-helm-release.injectable.ts
@@ -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;
+