From 786fa39c3302c8b69b8ecadb567edacb75e42042 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Sun, 5 Mar 2023 18:51:51 -0500 Subject: [PATCH] Add behavioural tests to cover bug fix Signed-off-by: Sebastian Malton --- .../authorization-review.injectable.ts | 50 -- .../create-authorization-api.injectable.ts | 16 + .../common/cluster/create-can-i.injectable.ts | 42 ++ .../cluster/create-core-api.injectable.ts | 16 + ...t-namespace-list-permissions.injectable.ts | 63 ++ .../cluster/list-namespaces.injectable.ts | 20 +- ...t-namespace-list-permissions.injectable.ts | 72 -- .../fs/copy.global-override-for-injectable.ts | 11 - .../lstat.global-override-for-injectable.ts | 11 - ...irectory.global-override-for-injectable.ts | 11 - .../remove.global-override-for-injectable.ts | 11 - ...ite-file.global-override-for-injectable.ts | 11 - .../refresh-accessibility-technical.test.ts | 652 ++++++++++++++++++ .../core/src/main/__test__/cluster.test.ts | 12 +- .../cluster/cluster-connection.injectable.ts | 34 +- .../request-api-resources.injectable.ts | 8 +- .../src/main/cluster/request-api-versions.ts | 7 +- .../request-core-api-versions.injectable.ts | 39 +- ...equest-non-core-api-versions.injectable.ts | 45 +- 19 files changed, 880 insertions(+), 251 deletions(-) delete mode 100644 packages/core/src/common/cluster/authorization-review.injectable.ts create mode 100644 packages/core/src/common/cluster/create-authorization-api.injectable.ts create mode 100644 packages/core/src/common/cluster/create-can-i.injectable.ts create mode 100644 packages/core/src/common/cluster/create-core-api.injectable.ts create mode 100644 packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts delete mode 100644 packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts delete mode 100644 packages/core/src/common/fs/copy.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/lstat.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/read-directory.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/remove.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/write-file.global-override-for-injectable.ts create mode 100644 packages/core/src/features/cluster/refresh-accessibility-technical.test.ts diff --git a/packages/core/src/common/cluster/authorization-review.injectable.ts b/packages/core/src/common/cluster/authorization-review.injectable.ts deleted file mode 100644 index 3352423377..0000000000 --- a/packages/core/src/common/cluster/authorization-review.injectable.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; - -/** - * Requests the permissions for actions on the kube cluster - * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed - * @returns `true` if the actions described are allowed - */ -export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI; - -const createAuthorizationReviewInjectable = getInjectable({ - id: "authorization-review", - instantiate: (di): CreateAuthorizationReview => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; - }; - }, -}); - -export default createAuthorizationReviewInjectable; diff --git a/packages/core/src/common/cluster/create-authorization-api.injectable.ts b/packages/core/src/common/cluster/create-authorization-api.injectable.ts new file mode 100644 index 0000000000..c0658a4a34 --- /dev/null +++ b/packages/core/src/common/cluster/create-authorization-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { AuthorizationV1Api } from "@kubernetes/client-node"; +import type { KubeConfig } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateAuthorizationApi = (config: KubeConfig) => AuthorizationV1Api; + +const createAuthorizationApiInjectable = getInjectable({ + id: "create-authorization-api", + instantiate: (): CreateAuthorizationApi => (config) => config.makeApiClient(AuthorizationV1Api), +}); + +export default createAuthorizationApiInjectable; diff --git a/packages/core/src/common/cluster/create-can-i.injectable.ts b/packages/core/src/common/cluster/create-can-i.injectable.ts new file mode 100644 index 0000000000..59d30dbe77 --- /dev/null +++ b/packages/core/src/common/cluster/create-can-i.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; + +/** + * Requests the permissions for actions on the kube cluster + * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed + * @returns `true` if the actions described are allowed + */ +export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; + +export type CreateCanI = (api: AuthorizationV1Api) => CanI; + +const createCanIInjectable = getInjectable({ + id: "create-can-i", + instantiate: (di): CreateCanI => { + const logger = di.inject(loggerInjectable); + + return (api) => async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }, +}); + +export default createCanIInjectable; diff --git a/packages/core/src/common/cluster/create-core-api.injectable.ts b/packages/core/src/common/cluster/create-core-api.injectable.ts new file mode 100644 index 0000000000..2389b12478 --- /dev/null +++ b/packages/core/src/common/cluster/create-core-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeConfig } from "@kubernetes/client-node"; +import { CoreV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateCoreApi = (config: KubeConfig) => CoreV1Api; + +const createCoreApiInjectable = getInjectable({ + id: "create-core-api", + instantiate: (): CreateCoreApi => config => config.makeApiClient(CoreV1Api), +}); + +export default createCoreApiInjectable; diff --git a/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts new file mode 100644 index 0000000000..1aca3dbe6e --- /dev/null +++ b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; +import type { KubeApiResource } from "../rbac"; + +export type CanListResource = (resource: KubeApiResource) => boolean; + +/** + * Requests the permissions for actions on the kube cluster + * @param namespace The namespace of the resources + */ +export type RequestNamespaceListPermissions = (namespace: string) => Promise; + +export type CreateRequestNamespaceListPermissions = (api: AuthorizationV1Api) => RequestNamespaceListPermissions; + +const createRequestNamespaceListPermissionsInjectable = getInjectable({ + id: "create-request-namespace-list-permissions", + instantiate: (di): CreateRequestNamespaceListPermissions => { + const logger = di.inject(loggerInjectable); + + return (api) => async (namespace) => { + try { + const { body: { status }} = await api.createSelfSubjectRulesReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectRulesReview", + spec: { namespace }, + }); + + if (!status || status.incomplete) { + logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); + + return () => true; + } + + const { resourceRules } = status; + + return (resource) => { + const rules = resourceRules.filter(({ + apiGroups = ["*"], resources = ["*"], + }) => { + const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group); + const isAboutResource = resources.includes("*") || resources.includes(resource.apiName); + + return isAboutRelevantApiGroup && isAboutResource; + }); + + return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list")); + }; + } catch (error) { + logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); + + return () => true; + } + }; + }, +}); + +export default createRequestNamespaceListPermissionsInjectable; diff --git a/packages/core/src/common/cluster/list-namespaces.injectable.ts b/packages/core/src/common/cluster/list-namespaces.injectable.ts index 363a10abb1..04acb0671b 100644 --- a/packages/core/src/common/cluster/list-namespaces.injectable.ts +++ b/packages/core/src/common/cluster/list-namespaces.injectable.ts @@ -2,27 +2,21 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { KubeConfig } from "@kubernetes/client-node"; -import { CoreV1Api } from "@kubernetes/client-node"; +import type { CoreV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { isDefined } from "@k8slens/utilities"; export type ListNamespaces = () => Promise; - -export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces; +export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces; const createListNamespacesInjectable = getInjectable({ id: "create-list-namespaces", - instantiate: (): CreateListNamespaces => (config) => { - const coreApi = config.makeApiClient(CoreV1Api); + instantiate: (): CreateListNamespaces => (api) => async () => { + const { body: { items }} = await api.listNamespace(); - return async () => { - const { body: { items }} = await coreApi.listNamespace(); - - return items - .map(ns => ns.metadata?.name) - .filter(isDefined); - }; + return items + .map(ns => ns.metadata?.name) + .filter(isDefined); }, }); diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts deleted file mode 100644 index 4b1aadeee6..0000000000 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; -import type { KubeApiResource } from "../rbac"; - -export type CanListResource = (resource: KubeApiResource) => boolean; - -/** - * Requests the permissions for actions on the kube cluster - * @param namespace The namespace of the resources - */ -export type RequestNamespaceListPermissions = (namespace: string) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type RequestNamespaceListPermissionsFor = (proxyConfig: KubeConfig) => RequestNamespaceListPermissions; - -const requestNamespaceListPermissionsForInjectable = getInjectable({ - id: "request-namespace-list-permissions-for", - instantiate: (di): RequestNamespaceListPermissionsFor => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (namespace) => { - try { - const { body: { status }} = await api.createSelfSubjectRulesReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectRulesReview", - spec: { namespace }, - }); - - if (!status || status.incomplete) { - logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); - - return () => true; - } - - const { resourceRules } = status; - - return (resource) => { - const rules = resourceRules.filter(({ - apiGroups = ["*"], - resources = ["*"], - }) => { - const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group); - const isAboutResource = resources.includes("*") || resources.includes(resource.apiName); - - return isAboutRelevantApiGroup && isAboutResource; - }); - - return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list")); - }; - } catch (error) { - logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); - - return () => true; - } - }; - }; - }, -}); - -export default requestNamespaceListPermissionsForInjectable; diff --git a/packages/core/src/common/fs/copy.global-override-for-injectable.ts b/packages/core/src/common/fs/copy.global-override-for-injectable.ts deleted file mode 100644 index 3799d3b760..0000000000 --- a/packages/core/src/common/fs/copy.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import copyInjectable from "./copy.injectable"; - -export default getGlobalOverride(copyInjectable, () => async () => { - throw new Error("tried to copy filepaths without override"); -}); diff --git a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts b/packages/core/src/common/fs/lstat.global-override-for-injectable.ts deleted file mode 100644 index 155fac7451..0000000000 --- a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import lstatInjectable from "./lstat.injectable"; - -export default getGlobalOverride(lstatInjectable, () => async () => { - throw new Error("tried to lstat a filepath without override"); -}); diff --git a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts b/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts deleted file mode 100644 index 72d9b523f4..0000000000 --- a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import readDirectoryInjectable from "./read-directory.injectable"; - -export default getGlobalOverride(readDirectoryInjectable, () => async () => { - throw new Error("tried to read a directory's content without override"); -}); diff --git a/packages/core/src/common/fs/remove.global-override-for-injectable.ts b/packages/core/src/common/fs/remove.global-override-for-injectable.ts deleted file mode 100644 index 58fb0f9dce..0000000000 --- a/packages/core/src/common/fs/remove.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import removePathInjectable from "./remove.injectable"; - -export default getGlobalOverride(removePathInjectable, () => async () => { - throw new Error("tried to remove path without override"); -}); diff --git a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts b/packages/core/src/common/fs/write-file.global-override-for-injectable.ts deleted file mode 100644 index e87f648305..0000000000 --- a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import writeFileInjectable from "./write-file.injectable"; - -export default getGlobalOverride(writeFileInjectable, () => async () => { - throw new Error("tried to write file without override"); -}); diff --git a/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts new file mode 100644 index 0000000000..a418d70900 --- /dev/null +++ b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts @@ -0,0 +1,652 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, CoreV1Api, V1APIGroupList, V1APIVersions, V1NamespaceList, V1SelfSubjectAccessReview, V1SelfSubjectRulesReview } from "@kubernetes/client-node"; +import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable"; +import createContextHandlerInjectable from "../../main/context-handler/create-context-handler.injectable"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import url from "url"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import type { PartialDeep } from "type-fest"; +import { anyObject } from "jest-mock-extended"; +import { flushPromises } from "../../common/test-utils/flush-promises"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; +import type { K8sRequest } from "../../main/k8s-request.injectable"; +import k8sRequestInjectable from "../../main/k8s-request.injectable"; +import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import detectClusterMetadataInjectable from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import { delay } from "../../common/utils"; + +describe("Refresh Cluster Accessibility Technical Tests", () => { + let builder: ApplicationBuilder; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let createSelfSubjectAccessReviewMock: AsyncFnMock; + let listNamespaceMock: AsyncFnMock; + let k8sRequestMock: AsyncFnMock; + let detectClusterMetadataMock: AsyncFnMock; + + beforeEach(async () => { + builder = getApplicationBuilder({ + fakeTime: false, + }); + + const mainDi = builder.mainDi; + + mainDi.override(broadcastMessageInjectable, () => async () => {}); + + detectClusterMetadataMock = asyncFn(); + mainDi.override(detectClusterMetadataInjectable, () => detectClusterMetadataMock); + + k8sRequestMock = asyncFn(); + mainDi.override(k8sRequestInjectable, () => k8sRequestMock); + + mainDi.override(createContextHandlerInjectable, () => (cluster) => { + const clusterUrl = url.parse(cluster.apiUrl); + + return { + clusterUrl, + ensureServer: async () => {}, + }; + }); + + createSelfSubjectRulesReviewMock = asyncFn(); + createSelfSubjectAccessReviewMock = asyncFn(); + mainDi.override(createAuthorizationApiInjectable, () => () => ({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + createSelfSubjectAccessReview: createSelfSubjectAccessReviewMock, + } as any)); + + listNamespaceMock = asyncFn(); + mainDi.override(createCoreApiInjectable, () => () => ({ + listNamespace: listNamespaceMock, + } as any)); + + await builder.render(); + }); + + describe("given a cluster with no configured preferences", () => { + let cluster: Cluster; + let refreshPromise: Promise; + + beforeEach(async () => { + const mainDi = builder.mainDi; + const clusterStore = mainDi.inject(clusterStoreInjectable); + const writeJsonFile = mainDi.inject(writeJsonFileInjectable); + + await writeJsonFile("/some-kube-config-path", { + apiVersion: "v1", + kind: "Config", + clusters: [{ + name: "some-cluster-name", + cluster: { + server: "https://localhost:8989", + }, + }], + users: [{ + name: "some-user-name", + }], + contexts: [{ + name: "some-cluster-context", + context: { + user: "some-user-name", + cluster: "some-cluster-name", + }, + }], + }); + + clusterStore.addCluster({ + contextName: "some-cluster-context", + id: "some-cluster-id", + kubeConfigPath: "/some-kube-config-path", + }); + + cluster = clusterStore.getById("some-cluster-id") ?? (() => { throw new Error("missing cluster"); })(); + refreshPromise = cluster.refreshAccessibilityAndMetadata(); + + // NOTE: I don't know why these are all are required to get the tests to pass + await flushPromises(); + await delay(50); + await flushPromises(); + }); + + it("requests if cluster has admin permissions", async () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "kube-system", + resource: "*", + verb: "create", + }, + })); + }); + + describe.each([ true, false ])("when cluster admin request resolves to %p", (isAdmin) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: isAdmin, + }, + } as PartialDeep, + } as any); + }); + + it("requests if cluster has global watch permissions", () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + verb: "watch", + resource: "*", + }, + })); + }); + + describe.each([ true, false ])("when cluster global watch request resolves with %p", (globalWatch) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: globalWatch, + }, + } as PartialDeep, + } as any); + }); + + it("requests namespaces", () => { + expect(listNamespaceMock).toBeCalled(); + }); + + describe("when list namespaces resolves", () => { + beforeEach(async () => { + await listNamespaceMock.resolve(listNamespaceResponse); + }); + + it("requests core api versions", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api", + ); + }); + + describe("when core api versions request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve({ + serverAddressByClientCIDRs: [], + versions: [ + "v1", + ], + } as V1APIVersions); + }); + + it("requests non-core api resource kinds", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis", + ); + }); + + describe("when non-core api resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nonCoreApiResponse); + }); + + it("requests specific resource kinds in core", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api/v1", + ); + }); + + describe("when core specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(coreApiKindsResponse); + }); + + it("requests specific resources kinds from the first non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/node.k8s.io/v1", + ); + }); + + describe("when first specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nodeK8sIoKindsResponse); + }); + + it("requests specific resources kinds from the second non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/discovery.k8s.io/v1", + ); + }); + + describe("when second specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(discoveryK8sIoKindsResponse); + }); + + it("requests namespace list permissions for 'default' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "default", + }, + })); + }); + + describe("when the permissions are incomplete", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultIncompletePermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.shouldShowResource({ + apiName: "pods", + group: "", + })).toBe(true); + }); + + it("should have the cluster displaying 'namespaces'", () => { + expect(cluster.shouldShowResource({ + apiName: "namespaces", + group: "", + })).toBe(true); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to an empty list", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.shouldShowResource({ + apiName: "pods", + group: "", + })).toBe(false); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.shouldShowResource({ + apiName: "namespaces", + group: "", + })).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to a single entry with 'list' verb", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultSingleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.shouldShowResource({ + apiName: "pods", + group: "", + })).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.shouldShowResource({ + apiName: "namespaces", + group: "", + })).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultMultipleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.shouldShowResource({ + apiName: "pods", + group: "", + })).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.shouldShowResource({ + apiName: "namespaces", + group: "", + })).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + }); + + describe.skip("when second specific resource kinds rejects", () => {}); + }); + }); + + describe.skip("when first specific resource kinds rejects", () => {}); + }); + }); + }); + }); + }); + }); +}); + +const nonCoreApiResponse = { + groups: [ + { + name: "node.k8s.io", + versions: [ + { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + }, + { + name: "discovery.k8s.io", + versions: [ + { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + }, + ], +} as V1APIGroupList; + +const listNamespaceResponse = { + body: { + items: [ + { + metadata: { + name: "default", + }, + }, + { + metadata: { + name: "my-namespace", + }, + }, + ], + } as PartialDeep, +} as Awaited>; + +const coreApiKindsResponse = { + kind: "APIResourceList", + groupVersion: "v1", + resources: [ + { + name: "namespaces", + singularName: "", + namespaced: false, + kind: "Namespace", + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], + shortNames: ["ns"], + storageVersionHash: "Q3oi5N2YM8M=", + }, + { + name: "pods", + singularName: "", + namespaced: true, + kind: "Pod", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + shortNames: ["po"], + categories: ["all"], + storageVersionHash: "xPOwRZ+Yhw8=", + }, + { + name: "pods/attach", + singularName: "", + namespaced: true, + kind: "PodAttachOptions", + verbs: ["create", "get"], + }, + ], +}; + +const nodeK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "node.k8s.io/v1", + resources: [ + { + name: "runtimeclasses", + singularName: "", + namespaced: false, + kind: "RuntimeClass", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "WQTu1GL3T2Q=", + }, + ], +}; + +const discoveryK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "discovery.k8s.io/v1", + resources: [ + { + name: "endpointslices", + singularName: "", + namespaced: true, + kind: "EndpointSlice", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "Nx3SIv6I0mE=", + }, + ], +}; + +type CreateSelfSubjectRulesReviewRes = Awaited>; + +const defaultIncompletePermissions = { + body: { + status: { + incomplete: true, + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const emptyPermissions = { + body: { + status: { + resourceRules: [], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultSingleListPermissions = { + body: { + status: { + resourceRules: [{ + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultMultipleListPermissions = { + body: { + status: { + resourceRules: [ + { + apiGroups: [""], + resources: ["pods"], + verbs: ["get"], + }, + { + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }, + ], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; diff --git a/packages/core/src/main/__test__/cluster.test.ts b/packages/core/src/main/__test__/cluster.test.ts index bb495af847..205a23d5ab 100644 --- a/packages/core/src/main/__test__/cluster.test.ts +++ b/packages/core/src/main/__test__/cluster.test.ts @@ -5,10 +5,6 @@ import { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; -import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; @@ -19,6 +15,10 @@ import clusterConnectionInjectable from "../cluster/cluster-connection.injectabl import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; describe("create clusters", () => { let cluster: Cluster; @@ -34,8 +34,8 @@ describe("create clusters", () => { di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); di.override(broadcastConnectionUpdateInjectable, () => async () => {}); - di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true)); - di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); + di.override(createCanIInjectable, () => () => () => Promise.resolve(true)); + di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(prometheusHandlerInjectable, () => ({ getPrometheusDetails: jest.fn(), diff --git a/packages/core/src/main/cluster/cluster-connection.injectable.ts b/packages/core/src/main/cluster/cluster-connection.injectable.ts index 433a9f5f23..8c85794e0b 100644 --- a/packages/core/src/main/cluster/cluster-connection.injectable.ts +++ b/packages/core/src/main/cluster/cluster-connection.injectable.ts @@ -6,10 +6,7 @@ import { type KubeConfig, HttpError } from "@kubernetes/client-node"; import { reaction, comparer, runInAction } from "mobx"; import { ClusterStatus } from "../../common/cluster-types"; -import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable"; -import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable"; import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; import type { Logger } from "../../common/logger"; @@ -25,7 +22,6 @@ import type { RequestApiResources } from "./request-api-resources.injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable"; import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable"; @@ -33,21 +29,31 @@ import loggerInjectable from "../../common/logger.injectable"; import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable"; import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable"; import requestApiResourcesInjectable from "./request-api-resources.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable"; import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token"; import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; import { replaceObservableObject } from "../../common/utils/replace-observable-object"; +import type { CreateAuthorizationApi } from "../../common/cluster/create-authorization-api.injectable"; +import type { CreateCanI } from "../../common/cluster/create-can-i.injectable"; +import type { CreateRequestNamespaceListPermissions, RequestNamespaceListPermissions } from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { CreateCoreApi } from "../../common/cluster/create-core-api.injectable"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; interface Dependencies { readonly logger: Logger; readonly prometheusHandler: ClusterPrometheusHandler; readonly kubeAuthProxyServer: KubeAuthProxyServer; readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector; - createAuthorizationReview: CreateAuthorizationReview; + createCanI: CreateCanI; requestApiResources: RequestApiResources; - requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; + createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions; + createAuthorizationApi: CreateAuthorizationApi; + createCoreApi: CreateCoreApi; createListNamespaces: CreateListNamespaces; detectClusterMetadata: DetectClusterMetadata; broadcastMessage: BroadcastMessage; @@ -224,8 +230,9 @@ class ClusterConnection { private async refreshAccessibility(): Promise { this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta()); const proxyConfig = await this.dependencies.loadProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); - const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + const api = this.dependencies.createAuthorizationApi(proxyConfig); + const canI = this.dependencies.createCanI(api); + const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api); const isAdmin = await canI({ namespace: "kube-system", @@ -360,7 +367,8 @@ class ClusterConnection { } try { - const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); + const api = this.dependencies.createCoreApi(proxyConfig); + const listNamespaces = this.dependencies.createListNamespaces(api); return await listNamespaces(); } catch (error) { @@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({ prometheusHandler: di.inject(prometheusHandlerInjectable, cluster), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), broadcastMessage: di.inject(broadcastMessageInjectable), - createAuthorizationReview: di.inject(createAuthorizationReviewInjectable), createListNamespaces: di.inject(createListNamespacesInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable), loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster), requestApiResources: di.inject(requestApiResourcesInjectable), - requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), + createAuthorizationApi: di.inject(createAuthorizationApiInjectable), + createCoreApi: di.inject(createCoreApiInjectable), + createCanI: di.inject(createCanIInjectable), + createRequestNamespaceListPermissions: di.inject(createRequestNamespaceListPermissionsInjectable), }, cluster, ), diff --git a/packages/core/src/main/cluster/request-api-resources.injectable.ts b/packages/core/src/main/cluster/request-api-resources.injectable.ts index d6a21e3ce4..dfd24b3425 100644 --- a/packages/core/src/main/cluster/request-api-resources.injectable.ts +++ b/packages/core/src/main/cluster/request-api-resources.injectable.ts @@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../common/logger.injectable"; import type { KubeApiResource } from "../../common/rbac"; import type { Cluster } from "../../common/cluster/cluster"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./request-api-versions"; import { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities"; import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import type { AsyncResult } from "@k8slens/utilities"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; +import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable"; export type RequestApiResources = (cluster: Cluster) => AsyncResult; @@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({ id: "request-api-resources", instantiate: (di): RequestApiResources => { const logger = di.inject(loggerInjectable); - const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken); + const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken) + .sort(byOrderNumber); const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable); return async (...args) => { @@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({ const groupLists: KubeResourceListGroup[] = []; for (const apiVersionRequester of apiVersionRequesters) { - const result = await backoffCaller(() => apiVersionRequester(cluster), { + const result = await backoffCaller(() => apiVersionRequester.request(cluster), { onIntermediateError: (error, attempt) => { broadcastConnectionUpdate({ message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, diff --git a/packages/core/src/main/cluster/request-api-versions.ts b/packages/core/src/main/cluster/request-api-versions.ts index a9b5074549..0bb2c65f6c 100644 --- a/packages/core/src/main/cluster/request-api-versions.ts +++ b/packages/core/src/main/cluster/request-api-versions.ts @@ -15,8 +15,11 @@ export interface ClusterData { readonly id: string; } -export type RequestApiVersions = (cluster: ClusterData) => AsyncResult; +export interface ApiVersionsRequester { + request(cluster: ClusterData): AsyncResult; + readonly orderNumber: number; +} -export const requestApiVersionsInjectionToken = getInjectionToken({ +export const apiVersionsRequesterInjectionToken = getInjectionToken({ id: "request-api-versions-token", }); diff --git a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts index b709eb9356..0e51ffea41 100644 --- a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts @@ -5,33 +5,36 @@ import type { V1APIVersions } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./request-api-versions"; const requestCoreApiVersionsInjectable = getInjectable({ id: "request-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; + return { + request: async (cluster) => { + try { + const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; - return { - callWasSuccessful: true, - response: versions.map(version => ({ - group: "", - path: `/api/${version}`, - })), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: versions.map(version => ({ + group: "", + path: `/api/${version}`, + })), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 10, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestCoreApiVersionsInjectable; diff --git a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts index 84e62f6b80..c943025bde 100644 --- a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts @@ -6,35 +6,40 @@ import type { V1APIGroupList } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { iter } from "@k8slens/utilities"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./request-api-versions"; const requestNonCoreApiVersionsInjectable = getInjectable({ id: "request-non-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; + return { + request: async (cluster) => { + try { + const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList; - return { - callWasSuccessful: true, - response: iter.chain(groups.values()) - .flatMap(group => group.versions.map(version => ({ - group: group.name, - path: `/apis/${version.groupVersion}`, - }))) - .collect(v => [...v]), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: iter.chain(groups.values()) + .flatMap((group) => + group.versions.map((version) => ({ + group: group.name, + path: `/apis/${version.groupVersion}`, + })), + ) + .collect((v) => [...v]), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 20, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestNonCoreApiVersionsInjectable;