mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix loading helm release details (#6318)
* Fix loading helm release details - The helm manifest can sometimes contain KubeJsonApiDataLists instead of just KubeJsonApiData entries - Add additional logging to main for when a route handler throws so that we can gain more context in the future Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix usage of getHelmReleaseResources Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add test to verify handling of Lists being returned Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
5e6cf163a2
commit
cdeededa82
@ -17,6 +17,7 @@ export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
|
|||||||
return Object.keys(obj) as K[];
|
return Object.keys(obj) as K[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function entries<K extends string, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
|
||||||
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
|
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
|
||||||
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];
|
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];
|
||||||
|
|
||||||
|
|||||||
@ -5552,11 +5552,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -5571,11 +5566,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -6784,11 +6774,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -6803,11 +6788,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -8016,11 +7996,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -8035,11 +8010,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -9073,11 +9043,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -9092,11 +9057,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -10132,11 +10092,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -10151,11 +10106,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -11364,11 +11314,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
Namespace
|
Namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
Age
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="TableRow"
|
class="TableRow"
|
||||||
@ -11383,11 +11328,6 @@ exports[`showing details for helm release given application is started when navi
|
|||||||
>
|
>
|
||||||
some-namespace
|
some-namespace
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="TableCell age"
|
|
||||||
>
|
|
||||||
0s
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import type { AsyncResult } from "../../../../../common/utils/async-result";
|
import type { AsyncResult } from "../../../../../common/utils/async-result";
|
||||||
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
|
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
|
import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../../common/k8s-api/kube-json-api";
|
||||||
|
|
||||||
const callForHelmManifestInjectable = getInjectable({
|
const callForHelmManifestInjectable = getInjectable({
|
||||||
id: "call-for-helm-manifest",
|
id: "call-for-helm-manifest",
|
||||||
@ -18,7 +18,7 @@ const callForHelmManifestInjectable = getInjectable({
|
|||||||
name: string,
|
name: string,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
kubeconfigPath: string,
|
kubeconfigPath: string,
|
||||||
): Promise<AsyncResult<KubeJsonApiData[]>> => {
|
): Promise<AsyncResult<(KubeJsonApiData | KubeJsonApiDataList)[]>> => {
|
||||||
const result = await execHelm([
|
const result = await execHelm([
|
||||||
"get",
|
"get",
|
||||||
"manifest",
|
"manifest",
|
||||||
|
|||||||
@ -3,48 +3,37 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
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";
|
import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
|
||||||
|
import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../common/k8s-api/kube-json-api";
|
||||||
|
import type { AsyncResult } from "../../../../common/utils/async-result";
|
||||||
|
|
||||||
export type GetHelmReleaseResources = (
|
export type GetHelmReleaseResources = (
|
||||||
name: string,
|
name: string,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
kubeconfigPath: string,
|
kubeconfigPath: string,
|
||||||
kubectlPath: string
|
) => Promise<AsyncResult<KubeJsonApiData[], string>>;
|
||||||
) => Promise<JsonObject[]>;
|
|
||||||
|
|
||||||
const getHelmReleaseResourcesInjectable = getInjectable({
|
const getHelmReleaseResourcesInjectable = getInjectable({
|
||||||
id: "get-helm-release-resources",
|
id: "get-helm-release-resources",
|
||||||
|
|
||||||
instantiate: (di): GetHelmReleaseResources => {
|
instantiate: (di): GetHelmReleaseResources => {
|
||||||
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
|
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
|
||||||
const callForKubeResourcesByManifest = di.inject(callForKubeResourcesByManifestInjectable);
|
|
||||||
|
|
||||||
return async (name, namespace, kubeconfigPath, kubectlPath) => {
|
return async (name, namespace, kubeconfigPath) => {
|
||||||
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
|
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
if (!result.callWasSuccessful) {
|
||||||
throw new Error(result.error);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await pipeline(
|
return {
|
||||||
result.response,
|
callWasSuccessful: true,
|
||||||
|
response: result.response.flatMap(item => (
|
||||||
groupBy((item) => item.metadata.namespace || namespace),
|
Array.isArray(item.items)
|
||||||
|
? (item as KubeJsonApiDataList).items
|
||||||
(x) => Object.entries(x),
|
: item as KubeJsonApiData
|
||||||
|
)),
|
||||||
map(([namespace, manifest]) =>
|
};
|
||||||
callForKubeResourcesByManifest(namespace, kubeconfigPath, kubectlPath, manifest),
|
|
||||||
),
|
|
||||||
|
|
||||||
promises => Promise.all(promises),
|
|
||||||
);
|
|
||||||
|
|
||||||
return results.flat(1);
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import type { ExecHelm } from "../../exec-helm/exec-helm.injectable";
|
|||||||
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
|
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
|
||||||
import type { AsyncFnMock } from "@async-fn/jest";
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
import asyncFn 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 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";
|
import execFileWithInputInjectable from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
|
||||||
|
import type { AsyncResult } from "../../../../common/utils/async-result";
|
||||||
|
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
|
||||||
|
|
||||||
describe("get helm release resources", () => {
|
describe("get helm release resources", () => {
|
||||||
let getHelmReleaseResources: GetHelmReleaseResources;
|
let getHelmReleaseResources: GetHelmReleaseResources;
|
||||||
@ -36,14 +37,13 @@ describe("get helm release resources", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when called", () => {
|
describe("when called", () => {
|
||||||
let actualPromise: Promise<JsonObject[]>;
|
let actualPromise: Promise<AsyncResult<KubeJsonApiData[], string>>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
actualPromise = getHelmReleaseResources(
|
actualPromise = getHelmReleaseResources(
|
||||||
"some-release",
|
"some-release",
|
||||||
"some-namespace",
|
"some-namespace",
|
||||||
"/some-kubeconfig-path",
|
"/some-kubeconfig-path",
|
||||||
"/some-kubectl-path",
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,14 +65,16 @@ describe("get helm release resources", () => {
|
|||||||
|
|
||||||
const actual = await actualPromise;
|
const actual = await actualPromise;
|
||||||
|
|
||||||
expect(actual).toEqual([]);
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when call for manifest resolves", () => {
|
it("when call to manifest resolves with resources, resolves with resources", async () => {
|
||||||
beforeEach(async () => {
|
await execHelmMock.resolve({
|
||||||
await execHelmMock.resolve({
|
callWasSuccessful: true,
|
||||||
callWasSuccessful: true,
|
response: `---
|
||||||
response: `---
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: SomeKind
|
kind: SomeKind
|
||||||
metadata:
|
metadata:
|
||||||
@ -80,135 +82,63 @@ metadata:
|
|||||||
namespace: some-namespace
|
namespace: some-namespace
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: SomeKind
|
kind: SomeOtherKind
|
||||||
metadata:
|
metadata:
|
||||||
name: some-resource-without-namespace
|
name: some-resource-without-namespace
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
kind: List
|
||||||
|
items:
|
||||||
|
- apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: collection-sumologic-fluentd-logs
|
||||||
|
namespace: some-namespace
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
kind: SomeKind
|
kind: SomeKind
|
||||||
metadata:
|
metadata:
|
||||||
name: some-resource-with-different-namespace
|
name: some-resource-with-different-namespace
|
||||||
namespace: some-other-namespace
|
namespace: some-other-namespace
|
||||||
---
|
---
|
||||||
`,
|
`,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls for resources from each namespace separately using the manifest as input", () => {
|
expect(await actualPromise).toEqual({
|
||||||
expect(execFileWithStreamInputMock.mock.calls).toEqual([
|
callWasSuccessful: true,
|
||||||
[
|
response: [
|
||||||
{
|
{
|
||||||
filePath: "/some-kubectl-path",
|
apiVersion: "v1",
|
||||||
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-namespace", "--output", "json"],
|
kind: "SomeKind",
|
||||||
input: `---
|
metadata: {
|
||||||
apiVersion: v1
|
name: "some-resource-with-same-namespace",
|
||||||
kind: SomeKind
|
namespace: "some-namespace",
|
||||||
metadata:
|
|
||||||
name: some-resource-with-same-namespace
|
|
||||||
namespace: some-namespace
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: SomeKind
|
|
||||||
metadata:
|
|
||||||
name: some-resource-without-namespace
|
|
||||||
---
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
|
{
|
||||||
[
|
apiVersion: "v1",
|
||||||
{
|
kind: "SomeOtherKind",
|
||||||
filePath: "/some-kubectl-path",
|
metadata: {
|
||||||
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-other-namespace", "--output", "json"],
|
name: "some-resource-without-namespace",
|
||||||
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,
|
apiVersion: "monitoring.coreos.com/v1",
|
||||||
|
kind: "ServiceMonitor",
|
||||||
response: JSON.stringify({
|
metadata: {
|
||||||
items: [{ some: "other-item" }],
|
name: "collection-sumologic-fluentd-logs",
|
||||||
|
namespace: "some-namespace",
|
||||||
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,
|
apiVersion: "v1",
|
||||||
|
kind: "SomeKind",
|
||||||
response: JSON.stringify({
|
metadata: {
|
||||||
items: [{ some: "item" }],
|
name: "some-resource-with-different-namespace",
|
||||||
|
namespace: "some-other-namespace",
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,12 +19,10 @@ const getHelmReleaseInjectable = getInjectable({
|
|||||||
|
|
||||||
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
||||||
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
||||||
const kubectl = await cluster.ensureKubectl();
|
|
||||||
const kubectlPath = await kubectl.getPath();
|
|
||||||
|
|
||||||
logger.debug("Fetch release");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
const args = [
|
const result = await execHelm([
|
||||||
"status",
|
"status",
|
||||||
releaseName,
|
releaseName,
|
||||||
"--namespace",
|
"--namespace",
|
||||||
@ -33,11 +31,11 @@ const getHelmReleaseInjectable = getInjectable({
|
|||||||
kubeconfigPath,
|
kubeconfigPath,
|
||||||
"--output",
|
"--output",
|
||||||
"json",
|
"json",
|
||||||
];
|
]);
|
||||||
|
|
||||||
const result = await execHelm(args);
|
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
if (!result.callWasSuccessful) {
|
||||||
|
logger.warn(`Failed to exectute helm: ${result.error}`);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,14 +45,22 @@ const getHelmReleaseInjectable = getInjectable({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
release.resources = await getHelmReleaseResources(
|
const resourcesResult = await getHelmReleaseResources(
|
||||||
releaseName,
|
releaseName,
|
||||||
namespace,
|
namespace,
|
||||||
kubeconfigPath,
|
kubeconfigPath,
|
||||||
kubectlPath,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return release;
|
if (!resourcesResult.callWasSuccessful) {
|
||||||
|
logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...release,
|
||||||
|
resources: resourcesResult.response,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
80
src/main/router/create-handler-for-route.injectable.ts
Normal file
80
src/main/router/create-handler-for-route.injectable.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ServerResponse } from "http";
|
||||||
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
|
import { object } from "../../common/utils";
|
||||||
|
import type { LensApiRequest, Route } from "./route";
|
||||||
|
import { contentTypes } from "./router-content-types";
|
||||||
|
|
||||||
|
export type RouteHandler = (request: LensApiRequest<string>, response: ServerResponse) => Promise<void>;
|
||||||
|
export type CreateHandlerForRoute = (route: Route<unknown, string>) => RouteHandler;
|
||||||
|
|
||||||
|
interface LensServerResponse {
|
||||||
|
statusCode: number;
|
||||||
|
content: any;
|
||||||
|
headers: {
|
||||||
|
[name: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeServerResponseFor = (serverResponse: ServerResponse) => ({
|
||||||
|
statusCode,
|
||||||
|
content,
|
||||||
|
headers,
|
||||||
|
}: LensServerResponse) => {
|
||||||
|
serverResponse.statusCode = statusCode;
|
||||||
|
|
||||||
|
for (const [name, value] of object.entries(headers)) {
|
||||||
|
serverResponse.setHeader(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content instanceof Buffer) {
|
||||||
|
serverResponse.write(content);
|
||||||
|
serverResponse.end();
|
||||||
|
} else if (content) {
|
||||||
|
serverResponse.end(content);
|
||||||
|
} else {
|
||||||
|
serverResponse.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHandlerForRouteInjectable = getInjectable({
|
||||||
|
id: "create-handler-for-route",
|
||||||
|
instantiate: (di): CreateHandlerForRoute => {
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return (route) => async (request, response) => {
|
||||||
|
const writeServerResponse = writeServerResponseFor(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await route.handler(request);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 204,
|
||||||
|
response: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
} else if (!result.proxy) {
|
||||||
|
const contentType = result.contentType || contentTypes.json;
|
||||||
|
|
||||||
|
writeServerResponse(contentType.resultMapper(result));
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 500,
|
||||||
|
error: error ? String(error) : "unknown error",
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.error(`[ROUTER]: route ${route.path}, called with ${request.path}, threw an error`, error);
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createHandlerForRouteInjectable;
|
||||||
@ -7,6 +7,7 @@ import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
|||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import parseRequestInjectable from "./parse-request.injectable";
|
import parseRequestInjectable from "./parse-request.injectable";
|
||||||
import type { Route } from "./route";
|
import type { Route } from "./route";
|
||||||
|
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
||||||
|
|
||||||
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
||||||
id: "route-injection-token",
|
id: "route-injection-token",
|
||||||
@ -24,13 +25,11 @@ export function getRouteInjectable<T, Path extends string>(
|
|||||||
const routerInjectable = getInjectable({
|
const routerInjectable = getInjectable({
|
||||||
id: "router",
|
id: "router",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => new Router({
|
||||||
const routes = di.injectMany(routeInjectionToken);
|
parseRequest: di.inject(parseRequestInjectable),
|
||||||
|
routes: di.injectMany(routeInjectionToken),
|
||||||
return new Router(routes, {
|
createHandlerForRoute: di.inject(createHandlerForRouteInjectable),
|
||||||
parseRequest: di.inject(parseRequestInjectable),
|
}),
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default routerInjectable;
|
export default routerInjectable;
|
||||||
|
|||||||
@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
import Call from "@hapi/call";
|
import Call from "@hapi/call";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { toPairs } from "lodash/fp";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import { contentTypes } from "./router-content-types";
|
import type { LensApiRequest, Route } from "./route";
|
||||||
import type { LensApiRequest, LensApiResult, Route } from "./route";
|
|
||||||
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
|
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
|
||||||
import type { ParseRequest } from "./parse-request.injectable";
|
import type { ParseRequest } from "./parse-request.injectable";
|
||||||
|
import type { CreateHandlerForRoute, RouteHandler } from "./create-handler-for-route.injectable";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
@ -22,15 +21,17 @@ export interface RouterRequestOpts {
|
|||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
parseRequest: ParseRequest;
|
parseRequest: ParseRequest;
|
||||||
|
createHandlerForRoute: CreateHandlerForRoute;
|
||||||
|
readonly routes: Route<unknown, string>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
protected router = new Call.Router<ReturnType<typeof handleRoute>>();
|
private readonly router = new Call.Router<RouteHandler>();
|
||||||
|
|
||||||
constructor(routes: Route<unknown, string>[], private dependencies: Dependencies) {
|
constructor(private readonly dependencies: Dependencies) {
|
||||||
routes.forEach(route => {
|
for (const route of this.dependencies.routes) {
|
||||||
this.router.add({ method: route.method, path: route.path }, handleRoute(route));
|
this.router.add({ method: route.method, path: route.path }, this.dependencies.createHandlerForRoute(route));
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||||
@ -52,7 +53,6 @@ export class Router {
|
|||||||
|
|
||||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
|
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
|
||||||
const { req, res, url, cluster, params } = opts;
|
const { req, res, url, cluster, params } = opts;
|
||||||
|
|
||||||
const { payload } = await this.dependencies.parseRequest(req, null, {
|
const { payload } = await this.dependencies.parseRequest(req, null, {
|
||||||
parse: true,
|
parse: true,
|
||||||
output: "data",
|
output: "data",
|
||||||
@ -70,77 +70,3 @@ export class Router {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRoute = (route: Route<unknown, string>) => async (request: LensApiRequest<string>, response: http.ServerResponse) => {
|
|
||||||
let result: LensApiResult<any> | void;
|
|
||||||
|
|
||||||
const writeServerResponse = writeServerResponseFor(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await route.handler(request);
|
|
||||||
} catch(error) {
|
|
||||||
const mappedResult = contentTypes.txt.resultMapper({
|
|
||||||
statusCode: 500,
|
|
||||||
error: error ? String(error) : "unknown error",
|
|
||||||
});
|
|
||||||
|
|
||||||
writeServerResponse(mappedResult);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
const mappedResult = contentTypes.txt.resultMapper({
|
|
||||||
statusCode: 204,
|
|
||||||
response: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
writeServerResponse(mappedResult);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.proxy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = result.contentType || contentTypes.json;
|
|
||||||
|
|
||||||
const mappedResult = contentType.resultMapper(result);
|
|
||||||
|
|
||||||
writeServerResponse(mappedResult);
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeServerResponseFor =
|
|
||||||
(serverResponse: http.ServerResponse) =>
|
|
||||||
({
|
|
||||||
statusCode,
|
|
||||||
content,
|
|
||||||
headers,
|
|
||||||
}: {
|
|
||||||
statusCode: number;
|
|
||||||
content: any;
|
|
||||||
headers: { [name: string]: string };
|
|
||||||
}) => {
|
|
||||||
serverResponse.statusCode = statusCode;
|
|
||||||
|
|
||||||
const headerPairs = toPairs<string>(headers);
|
|
||||||
|
|
||||||
headerPairs.forEach(([name, value]) => {
|
|
||||||
serverResponse.setHeader(name, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (content instanceof Buffer) {
|
|
||||||
serverResponse.write(content);
|
|
||||||
|
|
||||||
serverResponse.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
serverResponse.end(content);
|
|
||||||
} else {
|
|
||||||
serverResponse.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { kebabCase } from "lodash/fp";
|
|||||||
import { Badge } from "../../badge";
|
import { Badge } from "../../badge";
|
||||||
import { SubTitle } from "../../layout/sub-title";
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||||
import { ReactiveDuration } from "../../duration/reactive-duration";
|
|
||||||
import { Checkbox } from "../../checkbox";
|
import { Checkbox } from "../../checkbox";
|
||||||
import { MonacoEditor } from "../../monaco-editor";
|
import { MonacoEditor } from "../../monaco-editor";
|
||||||
import { Spinner } from "../../spinner";
|
import { Spinner } from "../../spinner";
|
||||||
@ -131,12 +130,10 @@ const ResourceGroup = ({
|
|||||||
<TableCell className="name">Name</TableCell>
|
<TableCell className="name">Name</TableCell>
|
||||||
|
|
||||||
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
|
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
|
||||||
|
|
||||||
<TableCell className="age">Age</TableCell>
|
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
{resources.map(
|
{resources.map(
|
||||||
({ creationTimestamp, detailsUrl, name, namespace, uid }) => (
|
({ detailsUrl, name, namespace, uid }) => (
|
||||||
<TableRow key={uid}>
|
<TableRow key={uid}>
|
||||||
<TableCell className="name">
|
<TableCell className="name">
|
||||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||||
@ -145,10 +142,6 @@ const ResourceGroup = ({
|
|||||||
{isNamespaced && (
|
{isNamespaced && (
|
||||||
<TableCell className="namespace">{namespace}</TableCell>
|
<TableCell className="namespace">{namespace}</TableCell>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TableCell className="age">
|
|
||||||
<ReactiveDuration timestamp={creationTimestamp} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -222,12 +222,12 @@ export class ReleaseDetailsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get notes() {
|
@computed get notes() {
|
||||||
return this.details.info.notes;
|
return this.details?.info.notes ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get groupedResources(): MinimalResourceGroup[] {
|
@computed get groupedResources(): MinimalResourceGroup[] {
|
||||||
return pipeline(
|
return pipeline(
|
||||||
this.details.resources,
|
this.details?.resources ?? [],
|
||||||
groupBy((resource) => resource.kind),
|
groupBy((resource) => resource.kind),
|
||||||
(grouped) => Object.entries(grouped),
|
(grouped) => Object.entries(grouped),
|
||||||
|
|
||||||
@ -271,20 +271,17 @@ export interface MinimalResource {
|
|||||||
name: string;
|
name: string;
|
||||||
namespace: string | undefined;
|
namespace: string | undefined;
|
||||||
detailsUrl: string | undefined;
|
detailsUrl: string | undefined;
|
||||||
creationTimestamp: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toMinimalResourceFor =
|
const toMinimalResourceFor =
|
||||||
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
|
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
|
||||||
(resource: KubeJsonApiData): MinimalResource => {
|
(resource: KubeJsonApiData): MinimalResource => {
|
||||||
const { creationTimestamp, name, namespace, uid } = resource.metadata;
|
const { name, namespace, uid } = resource.metadata;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid,
|
uid,
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
creationTimestamp,
|
|
||||||
|
|
||||||
detailsUrl: getResourceDetailsUrl(
|
detailsUrl: getResourceDetailsUrl(
|
||||||
kind,
|
kind,
|
||||||
resource.apiVersion,
|
resource.apiVersion,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import type { AsyncResult } from "../../../../../common/utils/async-result";
|
|||||||
|
|
||||||
export interface DetailedHelmRelease {
|
export interface DetailedHelmRelease {
|
||||||
release: HelmReleaseDto;
|
release: HelmReleaseDto;
|
||||||
details: HelmReleaseDetails;
|
details?: HelmReleaseDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestDetailedHelmRelease = (
|
export type RequestDetailedHelmRelease = (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user