diff --git a/src/common/utils/objects.ts b/src/common/utils/objects.ts index 8fe95be46d..9b6355d3e7 100644 --- a/src/common/utils/objects.ts +++ b/src/common/utils/objects.ts @@ -17,6 +17,7 @@ export function keys(obj: Record): K[] { return Object.keys(obj) as K[]; } +export function entries(obj: Partial> | null | undefined): [K, V][]; export function entries(obj: Partial> | null | undefined): [K, V][]; export function entries(obj: Record | null | undefined): [K, V][]; diff --git a/src/features/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap b/src/features/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap index a4159c60be..c45e3d30b6 100644 --- a/src/features/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap +++ b/src/features/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap @@ -5552,11 +5552,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
@@ -6784,11 +6774,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
@@ -8016,11 +7996,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
@@ -9073,11 +9043,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
@@ -10132,11 +10092,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
@@ -11364,11 +11314,6 @@ exports[`showing details for helm release given application is started when navi > Namespace -
- Age -
some-namespace
-
- 0s -
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 index 490c67273e..f0eefd720c 100644 --- 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 @@ -6,7 +6,7 @@ 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"; -import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api"; +import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../../common/k8s-api/kube-json-api"; const callForHelmManifestInjectable = getInjectable({ id: "call-for-helm-manifest", @@ -18,7 +18,7 @@ const callForHelmManifestInjectable = getInjectable({ name: string, namespace: string, kubeconfigPath: string, - ): Promise> => { + ): Promise> => { const result = await execHelm([ "get", "manifest", 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 index 025aab6b4d..ce5c60d6b3 100644 --- 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 @@ -3,48 +3,37 @@ * 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"; +import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../common/k8s-api/kube-json-api"; +import type { AsyncResult } from "../../../../common/utils/async-result"; export type GetHelmReleaseResources = ( name: string, namespace: string, kubeconfigPath: string, - kubectlPath: string -) => Promise; +) => 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) => { + return async (name, namespace, kubeconfigPath) => { const result = await callForHelmManifest(name, namespace, kubeconfigPath); if (!result.callWasSuccessful) { - throw new Error(result.error); + return result; } - 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); + return { + callWasSuccessful: true, + response: result.response.flatMap(item => ( + Array.isArray(item.items) + ? (item as KubeJsonApiDataList).items + : item as KubeJsonApiData + )), + }; }; }, }); 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 index 6c1b1c0566..b10bb650f1 100644 --- 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 @@ -10,9 +10,10 @@ 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"; +import type { AsyncResult } from "../../../../common/utils/async-result"; +import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api"; describe("get helm release resources", () => { let getHelmReleaseResources: GetHelmReleaseResources; @@ -36,14 +37,13 @@ describe("get helm release resources", () => { }); describe("when called", () => { - let actualPromise: Promise; + let actualPromise: Promise>; beforeEach(() => { actualPromise = getHelmReleaseResources( "some-release", "some-namespace", "/some-kubeconfig-path", - "/some-kubectl-path", ); }); @@ -65,14 +65,16 @@ describe("get helm release resources", () => { const actual = await actualPromise; - expect(actual).toEqual([]); + expect(actual).toEqual({ + callWasSuccessful: true, + response: [], + }); }); - describe("when call for manifest resolves", () => { - beforeEach(async () => { - await execHelmMock.resolve({ - callWasSuccessful: true, - response: `--- + it("when call to manifest resolves with resources, resolves with resources", async () => { + await execHelmMock.resolve({ + callWasSuccessful: true, + response: `--- apiVersion: v1 kind: SomeKind metadata: @@ -80,135 +82,63 @@ metadata: namespace: some-namespace --- apiVersion: v1 -kind: SomeKind +kind: SomeOtherKind metadata: name: some-resource-without-namespace --- 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 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 ---- -`, + expect(await actualPromise).toEqual({ + callWasSuccessful: true, + response: [ + { + apiVersion: "v1", + kind: "SomeKind", + metadata: { + name: "some-resource-with-same-namespace", + namespace: "some-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 ---- -`, + }, + { + apiVersion: "v1", + kind: "SomeOtherKind", + metadata: { + name: "some-resource-without-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: "", - }, - }), + apiVersion: "monitoring.coreos.com/v1", + kind: "ServiceMonitor", + metadata: { + name: "collection-sumologic-fluentd-logs", + namespace: "some-namespace", + }, }, - ); - - 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: "", - }, - }), + apiVersion: "v1", + kind: "SomeKind", + metadata: { + name: "some-resource-with-different-namespace", + namespace: "some-other-namespace", + }, }, - ); - - 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 3733b2816d..71d3fe6184 100644 --- a/src/main/helm/helm-service/get-helm-release.injectable.ts +++ b/src/main/helm/helm-service/get-helm-release.injectable.ts @@ -19,12 +19,10 @@ const getHelmReleaseInjectable = getInjectable({ return async (cluster: Cluster, releaseName: string, namespace: string) => { const kubeconfigPath = await cluster.getProxyKubeconfigPath(); - const kubectl = await cluster.ensureKubectl(); - const kubectlPath = await kubectl.getPath(); logger.debug("Fetch release"); - const args = [ + const result = await execHelm([ "status", releaseName, "--namespace", @@ -33,11 +31,11 @@ const getHelmReleaseInjectable = getInjectable({ kubeconfigPath, "--output", "json", - ]; - - const result = await execHelm(args); + ]); if (!result.callWasSuccessful) { + logger.warn(`Failed to exectute helm: ${result.error}`); + return undefined; } @@ -47,14 +45,22 @@ const getHelmReleaseInjectable = getInjectable({ return undefined; } - release.resources = await getHelmReleaseResources( + const resourcesResult = await getHelmReleaseResources( releaseName, namespace, kubeconfigPath, - kubectlPath, ); - return release; + if (!resourcesResult.callWasSuccessful) { + logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`); + + return undefined; + } + + return { + ...release, + resources: resourcesResult.response, + }; }; }, diff --git a/src/main/router/create-handler-for-route.injectable.ts b/src/main/router/create-handler-for-route.injectable.ts new file mode 100644 index 0000000000..60f62fd282 --- /dev/null +++ b/src/main/router/create-handler-for-route.injectable.ts @@ -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, response: ServerResponse) => Promise; +export type CreateHandlerForRoute = (route: Route) => 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; diff --git a/src/main/router/router.injectable.ts b/src/main/router/router.injectable.ts index f9d98d8a14..896f875332 100644 --- a/src/main/router/router.injectable.ts +++ b/src/main/router/router.injectable.ts @@ -7,6 +7,7 @@ import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; import { Router } from "./router"; import parseRequestInjectable from "./parse-request.injectable"; import type { Route } from "./route"; +import createHandlerForRouteInjectable from "./create-handler-for-route.injectable"; export const routeInjectionToken = getInjectionToken>({ id: "route-injection-token", @@ -24,13 +25,11 @@ export function getRouteInjectable( const routerInjectable = getInjectable({ id: "router", - instantiate: (di) => { - const routes = di.injectMany(routeInjectionToken); - - return new Router(routes, { - parseRequest: di.inject(parseRequestInjectable), - }); - }, + instantiate: (di) => new Router({ + parseRequest: di.inject(parseRequestInjectable), + routes: di.injectMany(routeInjectionToken), + createHandlerForRoute: di.inject(createHandlerForRouteInjectable), + }), }); export default routerInjectable; diff --git a/src/main/router/router.ts b/src/main/router/router.ts index 5b07822142..9cb070ed1d 100644 --- a/src/main/router/router.ts +++ b/src/main/router/router.ts @@ -5,12 +5,11 @@ import Call from "@hapi/call"; import type http from "http"; -import { toPairs } from "lodash/fp"; import type { Cluster } from "../../common/cluster/cluster"; -import { contentTypes } from "./router-content-types"; -import type { LensApiRequest, LensApiResult, Route } from "./route"; +import type { LensApiRequest, Route } from "./route"; import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy"; import type { ParseRequest } from "./parse-request.injectable"; +import type { CreateHandlerForRoute, RouteHandler } from "./create-handler-for-route.injectable"; export interface RouterRequestOpts { req: http.IncomingMessage; @@ -22,15 +21,17 @@ export interface RouterRequestOpts { interface Dependencies { parseRequest: ParseRequest; + createHandlerForRoute: CreateHandlerForRoute; + readonly routes: Route[]; } export class Router { - protected router = new Call.Router>(); + private readonly router = new Call.Router(); - constructor(routes: Route[], private dependencies: Dependencies) { - routes.forEach(route => { - this.router.add({ method: route.method, path: route.path }, handleRoute(route)); - }); + constructor(private readonly dependencies: Dependencies) { + for (const route of this.dependencies.routes) { + 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 { @@ -52,7 +53,6 @@ export class Router { protected async getRequest(opts: RouterRequestOpts): Promise> { const { req, res, url, cluster, params } = opts; - const { payload } = await this.dependencies.parseRequest(req, null, { parse: true, output: "data", @@ -70,77 +70,3 @@ export class Router { }; } } - -const handleRoute = (route: Route) => async (request: LensApiRequest, response: http.ServerResponse) => { - let result: LensApiResult | 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(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(); - } - }; diff --git a/src/renderer/components/+helm-releases/release-details/release-details-content.tsx b/src/renderer/components/+helm-releases/release-details/release-details-content.tsx index b2bacbd479..02e3255304 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details-content.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details-content.tsx @@ -19,7 +19,6 @@ import { kebabCase } from "lodash/fp"; import { Badge } from "../../badge"; import { SubTitle } from "../../layout/sub-title"; import { Table, TableCell, TableHead, TableRow } from "../../table"; -import { ReactiveDuration } from "../../duration/reactive-duration"; import { Checkbox } from "../../checkbox"; import { MonacoEditor } from "../../monaco-editor"; import { Spinner } from "../../spinner"; @@ -131,12 +130,10 @@ const ResourceGroup = ({ Name {isNamespaced && Namespace} - - Age {resources.map( - ({ creationTimestamp, detailsUrl, name, namespace, uid }) => ( + ({ detailsUrl, name, namespace, uid }) => ( {detailsUrl ? {name} : name} @@ -145,10 +142,6 @@ const ResourceGroup = ({ {isNamespaced && ( {namespace} )} - - - - ), )} diff --git a/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx b/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx index 66461ccd02..fc9a1ee9a9 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx @@ -222,12 +222,12 @@ export class ReleaseDetailsModel { } @computed get notes() { - return this.details.info.notes; + return this.details?.info.notes ?? ""; } @computed get groupedResources(): MinimalResourceGroup[] { return pipeline( - this.details.resources, + this.details?.resources ?? [], groupBy((resource) => resource.kind), (grouped) => Object.entries(grouped), @@ -271,20 +271,17 @@ export interface MinimalResource { name: string; namespace: string | undefined; detailsUrl: string | undefined; - creationTimestamp: string | undefined; } const toMinimalResourceFor = (getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) => (resource: KubeJsonApiData): MinimalResource => { - const { creationTimestamp, name, namespace, uid } = resource.metadata; + const { name, namespace, uid } = resource.metadata; return { uid, name, namespace, - creationTimestamp, - detailsUrl: getResourceDetailsUrl( kind, resource.apiVersion, diff --git a/src/renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts b/src/renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts index 6c7da31f97..aa5a2e9121 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts +++ b/src/renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts @@ -11,7 +11,7 @@ import type { AsyncResult } from "../../../../../common/utils/async-result"; export interface DetailedHelmRelease { release: HelmReleaseDto; - details: HelmReleaseDetails; + details?: HelmReleaseDetails; } export type RequestDetailedHelmRelease = (