From 886c8126791467c05446250513bc1d170350a8f5 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 20 Mar 2023 13:27:44 -0400 Subject: [PATCH] Simplify CRD KubeApi registrations - Switch to auto injectable registrations Signed-off-by: Sebastian Malton --- .../k8s-api/__tests__/api-manager.test.ts | 57 +++++++++++++++++++ .../common/k8s-api/api-manager/api-manager.ts | 7 +++ .../auto-registration-emitter.injectable.ts | 2 - .../k8s-api/api-manager/crd-api-token.ts | 11 ++++ .../k8s-api/api-manager/manager.injectable.ts | 4 ++ .../src/common/utils/registrator-helper.ts | 25 ++++++++ .../extension/extension.injectable.ts | 31 ++-------- ...setup-auto-crd-api-creations.injectable.ts | 53 +++++++++++++++++ .../setup-auto-registration.injectable.ts | 7 --- .../definition.store.injectable.ts | 2 - .../+custom-resources/definition.store.ts | 19 +------ 11 files changed, 163 insertions(+), 55 deletions(-) create mode 100644 packages/core/src/common/k8s-api/api-manager/crd-api-token.ts create mode 100644 packages/core/src/common/utils/registrator-helper.ts create mode 100644 packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts diff --git a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts index 6ea6327038..a1c22a227d 100644 --- a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts @@ -4,6 +4,7 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; @@ -21,6 +22,8 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; // eslint-disable-next-line no-restricted-imports import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { Cluster } from "../../cluster/cluster"; +import { runInAction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token"; class TestApi extends KubeApi { protected async checkPreferredVersion() { @@ -117,4 +120,58 @@ describe("ApiManager", () => { }); }); }); + + describe("given than a CRD has a default KubeApi registered for it", () => { + const apiBase = "/apis/aquasecurity.github.io/v1alpha1/vulnerabilityreports"; + + beforeEach(() => { + runInAction(() => { + di.register(getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${apiBase}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = "VulnerabilityReport"; + static readonly namespaced = true; + static readonly apiBase = apiBase; + }; + + return Object.assign( + new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }), + { + myField: 1, + }, + ); + }, + injectionToken: customResourceDefinitionApiInjectionToken, + })); + }); + }); + + it("can be retrieved from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 1, + }); + }); + + describe("given that an extension registers an api with the same apibase", () => { + beforeEach(() => { + void Object.assign(new ExternalKubeApi({ + objectConstructor: KubeObject, + apiBase, + kind: "VulnerabilityReport", + }), { + myField: 2, + }); + }); + + it("the extension's instance is retrievable instead from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 2, + }); + }); + }); + }); }); diff --git a/packages/core/src/common/k8s-api/api-manager/api-manager.ts b/packages/core/src/common/k8s-api/api-manager/api-manager.ts index f6c2758921..f73892d0e3 100644 --- a/packages/core/src/common/k8s-api/api-manager/api-manager.ts +++ b/packages/core/src/common/k8s-api/api-manager/api-manager.ts @@ -26,6 +26,7 @@ export type FindApiCallback = (api: KubeApi) => boolean; interface Dependencies { readonly apis: IComputedValue; + readonly crdApis: IComputedValue; readonly stores: IComputedValue; } @@ -56,6 +57,12 @@ export class ApiManager { } } + for (const crdApi of this.dependencies.crdApis.get()) { + if (!newState.has(crdApi.apiBase)) { + newState.set(crdApi.apiBase, crdApi); + } + } + this.apis.replace(newState); }); } diff --git a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts index d9a68a988c..714e9d8952 100644 --- a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts @@ -5,11 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import EventEmitter from "events"; import type TypedEventEmitter from "typed-emitter"; -import type { CustomResourceDefinition } from "../endpoints"; import type { KubeApi } from "../kube-api"; export interface LegacyAutoRegistration { - customResourceDefinition: (crd: CustomResourceDefinition) => void; kubeApi: (api: KubeApi) => void; } diff --git a/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts new file mode 100644 index 0000000000..d8f0c918c2 --- /dev/null +++ b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { KubeApi } from "../kube-api"; + +export const customResourceDefinitionApiInjectionToken = getInjectionToken({ + id: "custom-resource-definition-api-token", +}); diff --git a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts index f0b61c28b6..2f2615b471 100644 --- a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts @@ -9,6 +9,7 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { computed } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "./crd-api-token"; const apiManagerInjectable = getInjectable({ id: "api-manager", @@ -23,6 +24,9 @@ const apiManagerInjectable = getInjectable({ stores: storesAndApisCanBeCreated ? computedInjectMany(kubeObjectStoreInjectionToken) : computed(() => []), + crdApis: storesAndApisCanBeCreated + ? computedInjectMany(customResourceDefinitionApiInjectionToken) + : computed(() => []), }); }, }); diff --git a/packages/core/src/common/utils/registrator-helper.ts b/packages/core/src/common/utils/registrator-helper.ts new file mode 100644 index 0000000000..d9fa03c680 --- /dev/null +++ b/packages/core/src/common/utils/registrator-helper.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { iter } from "@k8slens/utilities"; +import type { DiContainerForInjection, Injectable } from "@ogre-tools/injectable"; + +// Register new injectables and deregister removed injectables by id + +export const injectableDifferencingRegistratorWith = (di: DiContainerForInjection) => ( + (rawCurrent: Injectable[], rawPrevious: Injectable[] = []) => { + const current = new Map(rawCurrent.map(inj => [inj.id, inj])); + const previous = new Map(rawPrevious.map(inj => [inj.id, inj])); + const toAdd = iter.chain(current.entries()) + .filter(([id]) => previous.has(id)) + .collect(entries => new Map(entries)); + const toRemove = iter.chain(previous.entries()) + .filter(([id]) => !current.has(id)) + .collect(entries => new Map(entries)); + + di.deregister(...toRemove.values()); + di.register(...toAdd.values()); + } +); diff --git a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts index 07f054d3fd..2e86a1c651 100644 --- a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts +++ b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts @@ -2,29 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { difference, find, map } from "lodash"; import { reaction, runInAction } from "mobx"; import { disposer } from "@k8slens/utilities"; import type { LensExtension } from "../../lens-extension"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; export interface Extension { register: () => void; deregister: () => void; } -const idsToInjectables = (ids: string[], injectables: Injectable[]) => ids.map(id => { - const injectable = find(injectables, { id }); - - if (!injectable) { - throw new Error(`Injectable ${id} not found`); - } - - return injectable; -}); - const extensionInjectable = getInjectable({ id: "extension", @@ -35,6 +24,7 @@ const extensionInjectable = getInjectable({ instantiate: (childDi) => { const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const reactionDisposer = disposer(); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi); return { register: () => { @@ -46,21 +36,8 @@ const extensionInjectable = getInjectable({ // we need to update the registered injectables with a reaction every time they change reaction( () => Array.isArray(injectables) ? injectables : injectables.get(), - (currentInjectables, previousInjectables = []) => { - // Register new injectables and deregister removed injectables by id - const currentIds = map(currentInjectables, "id"); - const previousIds = map(previousInjectables, "id"); - const idsToAdd = difference(currentIds, previousIds); - const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId)); - - if (idsToRemove.length > 0) { - childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables)); - } - - if (idsToAdd.length > 0) { - childDi.register(...idsToInjectables(idsToAdd, currentInjectables)); - } - }, { + injectableDifferencingRegistrator, + { fireImmediately: true, }, )); diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts new file mode 100644 index 0000000000..c23a34d767 --- /dev/null +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts @@ -0,0 +1,53 @@ +/** + * 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 { reaction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../../../common/k8s-api/api-manager/crd-api-token"; +import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints"; +import { KubeApi } from "../../../common/k8s-api/kube-api"; +import { KubeObject } from "../../../common/k8s-api/kube-object"; +import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; +import customResourceDefinitionStoreInjectable from "../../components/+custom-resources/definition.store.injectable"; +import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; + +const setupAutoCrdApiCreationsInjectable = getInjectable({ + id: "setup-auto-crd-api-creations", + instantiate: (di) => ({ + run: () => { + const customResourceDefinitionStore = di.inject(customResourceDefinitionStoreInjectable); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(di); + + reaction( + () => customResourceDefinitionStore.getItems().map(toCrdApiInjectable), + injectableDifferencingRegistrator, + { + fireImmediately: true, + }, + ); + }, + }), + injectionToken: beforeClusterFrameStartsSecondInjectionToken, +}); + +export default setupAutoCrdApiCreationsInjectable; + +const toCrdApiInjectable = (crd: CustomResourceDefinition) => getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${crd.getResourceApiBase()}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = crd.getResourceKind(); + static readonly namespaced = crd.isNamespaced(); + static readonly apiBase = crd.getResourceApiBase(); + }; + + return new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }); + }, + injectionToken: customResourceDefinitionApiInjectionToken, +}); diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts index dbc77a92b8..dfb89669ff 100644 --- a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts @@ -63,13 +63,6 @@ const setupAutoRegistrationInjectable = getInjectable({ }; autoRegistrationEmitter - .on("customResourceDefinition", (crd) => { - if (initialized) { - autoInitCustomResourceStore(crd); - } else { - beforeApiManagerInitializationCrds.push(crd); - } - }) .on("kubeApi", (api) => { if (initialized) { autoInitKubeApi(api); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts index f1d8f635b8..dbbd2460aa 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import assert from "assert"; -import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({ const api = di.inject(customResourceDefinitionApiInjectable); return new CustomResourceDefinitionStore({ - autoRegistration: di.inject(autoRegistrationEmitterInjectable), context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable), logger: di.inject(loggerInjectable), }, api); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.ts index 310e51709a..78ef3982a9 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.ts @@ -3,37 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { computed, reaction, makeObservable } from "mobx"; +import { computed, makeObservable } from "mobx"; import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import type TypedEventEmitter from "typed-emitter"; -import type { LegacyAutoRegistration } from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import autoBind from "auto-bind"; -export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies { - readonly autoRegistration: TypedEventEmitter; -} - export class CustomResourceDefinitionStore extends KubeObjectStore { constructor( - protected readonly dependencies: CustomResourceDefinitionStoreDependencies, + dependencies: KubeObjectStoreDependencies, api: CustomResourceDefinitionApi, opts?: KubeObjectStoreOptions, ) { super(dependencies, api, opts); makeObservable(this); autoBind(this); - - reaction( - () => this.getItems(), - crds => { - for (const crd of crds) { - this.dependencies.autoRegistration.emit("customResourceDefinition", crd); - } - }, - ); } protected sortItems(items: CustomResourceDefinition[]) {