1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix behaviour of auto generated CRD KubeApis and KubeObjectStores (#7384)

* Simplify CRD KubeApi registrations

- Switch to auto injectable registrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make sure that stores can still be retrieved

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup get extension fake to simplify impl

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify logic for extensionInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test in differencing registrator

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup code style

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix some tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix HPA details tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-22 11:51:26 -04:00 committed by GitHub
parent 8a80607d85
commit 4b1d740d61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 315 additions and 189 deletions

View File

@ -4,6 +4,7 @@
*/ */
import type { DiContainer } from "@ogre-tools/injectable"; 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 clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
import { Cluster } from "../../cluster/cluster"; import { Cluster } from "../../cluster/cluster";
import { runInAction } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token";
import assert from "assert";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
@ -117,4 +121,90 @@ 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,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
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,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
describe("given that an extension registers a store for the same apibase", () => {
beforeEach(() => {
const api = apiManager.getApi(apiBase);
assert(api);
apiManager.registerStore(Object.assign(
new KubeObjectStore({
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
}, api),
{
someField: 2,
},
));
});
it("can gets the custom KubeObjectStore instance instead", () => {
expect(apiManager.getStore(apiBase)).toMatchObject({
someField: 2,
});
});
});
});
});
}); });

View File

@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
import type { KubeObject, ObjectReference } from "../kube-object"; import type { KubeObject, ObjectReference } from "../kube-object";
import { parseKubeApi, createKubeApiURL } from "../kube-api-parse"; import { parseKubeApi, createKubeApiURL } from "../kube-api-parse";
import { iter } from "@k8slens/utilities"; import { getOrInsertWith, iter } from "@k8slens/utilities";
import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable";
export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any> export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any>
? Store ? Store
@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
interface Dependencies { interface Dependencies {
readonly apis: IComputedValue<KubeApi[]>; readonly apis: IComputedValue<KubeApi[]>;
readonly crdApis: IComputedValue<KubeApi[]>;
readonly stores: IComputedValue<KubeObjectStore[]>; readonly stores: IComputedValue<KubeObjectStore[]>;
createCustomResourceStore: CreateCustomResourceStore;
} }
export class ApiManager { export class ApiManager {
private readonly externalApis = observable.array<KubeApi>(); private readonly externalApis = observable.array<KubeApi>();
private readonly externalStores = observable.array<KubeObjectStore>(); private readonly externalStores = observable.array<KubeObjectStore>();
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
private readonly apis = observable.map<string, KubeApi>(); private readonly apis = observable.map<string, KubeApi>();
constructor(private readonly dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
@ -56,6 +59,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); this.apis.replace(newState);
}); });
} }
@ -110,6 +119,16 @@ export class ApiManager {
this.externalStores.push(store); this.externalStores.push(store);
} }
private apiIsDefaultCrdApi(api: KubeApi): boolean {
for (const crdApi of this.dependencies.crdApis.get()) {
if (crdApi.apiBase === api.apiBase) {
return true;
}
}
return false;
}
getStore(api: string | undefined): KubeObjectStore | undefined; getStore(api: string | undefined): KubeObjectStore | undefined;
getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined; getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined;
/** /**
@ -130,9 +149,19 @@ export class ApiManager {
return undefined; return undefined;
} }
return iter.chain(this.dependencies.stores.get().values()) const defaultResult = iter.chain(this.dependencies.stores.get().values())
.concat(this.externalStores.values()) .concat(this.externalStores.values())
.find(store => store.api.apiBase === api.apiBase); .find(store => store.api.apiBase === api.apiBase);
if (defaultResult) {
return defaultResult;
}
if (this.apiIsDefaultCrdApi(api)) {
return getOrInsertWith(this.defaultCrdStores, api.apiBase, () => this.dependencies.createCustomResourceStore(api));
}
return undefined;
} }
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string { lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {

View File

@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { CustomResourceDefinition } from "../endpoints";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
export interface LegacyAutoRegistration { export interface LegacyAutoRegistration {
customResourceDefinition: (crd: CustomResourceDefinition) => void;
kubeApi: (api: KubeApi<any, any>) => void; kubeApi: (api: KubeApi<any, any>) => void;
} }

View File

@ -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<KubeApi>({
id: "custom-resource-definition-api-token",
});

View File

@ -0,0 +1,27 @@
/**
* 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 clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../logger.injectable";
import type { KubeApi } from "../kube-api";
import type { KubeObject } from "../kube-object";
import type { KubeObjectStoreDependencies } from "../kube-object.store";
import { CustomResourceStore } from "./resource.store";
export type CreateCustomResourceStore = <K extends KubeObject>(api: KubeApi<K>) => CustomResourceStore<K>;
const createCustomResourceStoreInjectable = getInjectable({
id: "create-custom-resource-store",
instantiate: (di): CreateCustomResourceStore => {
const deps: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
return (api) => new CustomResourceStore(deps, api);
},
});
export default createCustomResourceStoreInjectable;

View File

@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f
import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeObjectStoreInjectionToken } from "./kube-object-store-token";
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
import { computed } from "mobx"; import { computed } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "./crd-api-token";
import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable";
const apiManagerInjectable = getInjectable({ const apiManagerInjectable = getInjectable({
id: "api-manager", id: "api-manager",
@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({
stores: storesAndApisCanBeCreated stores: storesAndApisCanBeCreated
? computedInjectMany(kubeObjectStoreInjectionToken) ? computedInjectMany(kubeObjectStoreInjectionToken)
: computed(() => []), : computed(() => []),
crdApis: storesAndApisCanBeCreated
? computedInjectMany(customResourceDefinitionApiInjectionToken)
: computed(() => []),
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
}); });
}, },
}); });

View File

@ -88,7 +88,7 @@ export interface KubeObjectStoreDependencies {
readonly logger: Logger; readonly logger: Logger;
} }
export abstract class KubeObjectStore< export class KubeObjectStore<
K extends KubeObject = KubeObject, K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>, A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>, D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,

View File

@ -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<any, any, any>[], rawPrevious: Injectable<any, any, any>[] = []) => {
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());
}
);

View File

@ -2,29 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { difference, find, map } from "lodash";
import { reaction, runInAction } from "mobx"; import { reaction, runInAction } from "mobx";
import { disposer } from "@k8slens/utilities"; import { disposer } from "@k8slens/utilities";
import type { LensExtension } from "../../lens-extension"; import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
export interface Extension { export interface Extension {
register: () => void; register: () => void;
deregister: () => void; deregister: () => void;
} }
const idsToInjectables = (ids: string[], injectables: Injectable<any, any, any>[]) => ids.map(id => {
const injectable = find(injectables, { id });
if (!injectable) {
throw new Error(`Injectable ${id} not found`);
}
return injectable;
});
const extensionInjectable = getInjectable({ const extensionInjectable = getInjectable({
id: "extension", id: "extension",
@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({
instantiate: (childDi) => { instantiate: (childDi) => {
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
const reactionDisposer = disposer(); const reactionDisposer = disposer();
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi);
return { return {
register: () => { register: () => {
extensionRegistrators.forEach((getInjectablesOfExtension) => { for (const extensionRegistrator of extensionRegistrators) {
const injectables = getInjectablesOfExtension(instance); const injectables = extensionRegistrator(instance);
reactionDisposer.push( if (Array.isArray(injectables)) {
// injectables is either an array or a computed array, in which case runInAction(() => {
// we need to update the registered injectables with a reaction every time they change injectableDifferencingRegistrator(injectables);
reaction( });
() => Array.isArray(injectables) ? injectables : injectables.get(), } else {
(currentInjectables, previousInjectables = []) => { reactionDisposer.push(reaction(
// Register new injectables and deregister removed injectables by id () => injectables.get(),
const currentIds = map(currentInjectables, "id"); injectableDifferencingRegistrator,
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));
}
}, {
fireImmediately: true, fireImmediately: true,
}, },
)); ));
}); }
}
}, },
deregister: () => { deregister: () => {

View File

@ -31,8 +31,7 @@ describe("extension special characters in page registrations", () => {
describe("when navigating to route with ID having special characters", () => { describe("when navigating to route with ID having special characters", () => {
beforeEach(() => { beforeEach(() => {
const testExtension = const testExtension = builder.extensions.get("some-extension-id").applicationWindows.only;
builder.extensions.get("some-extension-id").applicationWindows.only;
testExtension.navigate("/some-page-id/"); testExtension.navigate("/some-page-id/");
}); });

View File

@ -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,
});

View File

@ -5,71 +5,22 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import { CustomResourceStore } from "../../../common/k8s-api/api-manager/resource.store"; import type { KubeApi } from "../../../common/k8s-api/kube-api";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import type { KubeApiDependencies } from "../../../common/k8s-api/kube-api";
import { KubeApi } from "../../../common/k8s-api/kube-api";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens";
import type { KubeObjectStoreDependencies } from "../../../common/k8s-api/kube-object.store";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable";
const setupAutoRegistrationInjectable = getInjectable({ const setupAutoRegistrationInjectable = getInjectable({
id: "setup-auto-registration", id: "setup-auto-registration",
instantiate: (di) => ({ instantiate: (di) => ({
run: () => { run: () => {
const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable); const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable);
const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = [];
const beforeApiManagerInitializationApis: KubeApi[] = []; const beforeApiManagerInitializationApis: KubeApi[] = [];
const kubeApiDependencies: KubeApiDependencies = {
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
};
const kubeObjectStoreDependencies: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
let initialized = false; let initialized = false;
const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
const api = (() => {
const rawApi = apiManager.getApi(objectConstructor.apiBase);
if (rawApi) {
return rawApi;
}
const api = new KubeApi(kubeApiDependencies, { objectConstructor });
apiManager.registerApi(api);
return api;
})();
if (!apiManager.getStore(api)) {
apiManager.registerStore(new CustomResourceStore(kubeObjectStoreDependencies, api));
}
};
const autoInitKubeApi = (api: KubeApi) => { const autoInitKubeApi = (api: KubeApi) => {
apiManager.registerApi(api); apiManager.registerApi(api);
}; };
autoRegistrationEmitter autoRegistrationEmitter
.on("customResourceDefinition", (crd) => {
if (initialized) {
autoInitCustomResourceStore(crd);
} else {
beforeApiManagerInitializationCrds.push(crd);
}
})
.on("kubeApi", (api) => { .on("kubeApi", (api) => {
if (initialized) { if (initialized) {
autoInitKubeApi(api); autoInitKubeApi(api);
@ -81,7 +32,6 @@ const setupAutoRegistrationInjectable = getInjectable({
// NOTE: this MUST happen after the event emitter listeners are registered // NOTE: this MUST happen after the event emitter listeners are registered
const apiManager = di.inject(apiManagerInjectable); const apiManager = di.inject(apiManagerInjectable);
beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore);
beforeApiManagerInitializationApis.forEach(autoInitKubeApi); beforeApiManagerInitializationApis.forEach(autoInitKubeApi);
initialized = true; initialized = true;
}, },

View File

@ -4,8 +4,13 @@
*/ */
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import React from "react"; import React from "react";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints"; import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { DiRender } from "../test-utils/renderFor"; import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor";
import { HpaDetails } from "./hpa-details"; import { HpaDetails } from "./hpa-details";
@ -41,6 +46,17 @@ describe("<HpaDetails/>", () => {
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting(); const di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di); render = renderFor(di);
}); });

View File

@ -4,7 +4,6 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert"; 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 { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token";
import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable"; import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({
const api = di.inject(customResourceDefinitionApiInjectable); const api = di.inject(customResourceDefinitionApiInjectable);
return new CustomResourceDefinitionStore({ return new CustomResourceDefinitionStore({
autoRegistration: di.inject(autoRegistrationEmitterInjectable),
context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable), context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
}, api); }, api);

View File

@ -3,37 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { computed, reaction, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store"; import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectStore } 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 { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
import type { KubeObject } from "../../../common/k8s-api/kube-object"; 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"; import autoBind from "auto-bind";
export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies {
readonly autoRegistration: TypedEventEmitter<LegacyAutoRegistration>;
}
export class CustomResourceDefinitionStore extends KubeObjectStore<CustomResourceDefinition, CustomResourceDefinitionApi> { export class CustomResourceDefinitionStore extends KubeObjectStore<CustomResourceDefinition, CustomResourceDefinitionApi> {
constructor( constructor(
protected readonly dependencies: CustomResourceDefinitionStoreDependencies, dependencies: KubeObjectStoreDependencies,
api: CustomResourceDefinitionApi, api: CustomResourceDefinitionApi,
opts?: KubeObjectStoreOptions, opts?: KubeObjectStoreOptions,
) { ) {
super(dependencies, api, opts); super(dependencies, api, opts);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
reaction(
() => this.getItems(),
crds => {
for (const crd of crds) {
this.dependencies.autoRegistration.emit("customResourceDefinition", crd);
}
},
);
} }
protected sortItems(items: CustomResourceDefinition[]) { protected sortItems(items: CustomResourceDefinition[]) {

View File

@ -53,7 +53,7 @@ import { applicationWindowInjectionToken } from "../../../main/start-main-applic
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { FakeExtensionOptions } from "./get-extension-fake"; import type { FakeExtensionOptions } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake"; import { getMainExtensionFakeWith, getRendererExtensionFakeWith } from "./get-extension-fake";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable"; import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints"; import { Namespace } from "../../../common/k8s-api/endpoints";
import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes"; import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
@ -594,49 +594,28 @@ export const getApplicationBuilder = () => {
}, },
enable: (...extensions) => { enable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => { builder.afterWindowStart(action(({ windowDi }) => {
const rendererExtensionInstances = extensions.map((options) => extensions
getExtensionFakeForRenderer( .map(getRendererExtensionFakeWith(windowDi))
windowDi, .forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable));
options.id, }));
options.name,
options.rendererOptions || {},
),
);
rendererExtensionInstances.forEach( builder.afterApplicationStart(action(({ mainDi }) => {
enableExtensionFor(windowDi, rendererExtensionsStateInjectable), extensions
); .map(getMainExtensionFakeWith(mainDi))
}); .forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable));
}));
builder.afterApplicationStart(({ mainDi }) => {
const mainExtensionInstances = extensions.map((extension) =>
getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}),
);
runInAction(() => {
mainExtensionInstances.forEach(
enableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
});
});
}, },
disable: (...extensions) => { disable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => { builder.afterWindowStart(({ windowDi }) => {
extensions extensions
.map((ext) => ext.id) .forEach(disableExtensionFor(windowDi, rendererExtensionsStateInjectable));
.forEach(
disableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
}); });
builder.afterApplicationStart(({ mainDi }) => { builder.afterApplicationStart(({ mainDi }) => {
extensions extensions
.map((ext) => ext.id) .forEach(disableExtensionFor(mainDi, mainExtensionsStateInjectable));
.forEach(
disableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
}); });
}, },
}, },
@ -835,49 +814,29 @@ const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelT
userEvent.click(option); userEvent.click(option);
}; };
const enableExtensionFor = ( function enableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, any, any>) {
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, any, any>,
) => {
const extensionState = di.inject(stateInjectable); const extensionState = di.inject(stateInjectable);
const getExtension = (extension: LensExtension) => return (instance: LensExtension) => {
di.inject(extensionInjectable, extension); const extension = di.inject(extensionInjectable, instance);
return (extensionInstance: LensExtension) => {
const extension = getExtension(extensionInstance);
runInAction(() => {
extension.register(); extension.register();
extensionState.set(extensionInstance.id, extensionInstance); extensionState.set(instance.id, instance);
});
}; };
};
const disableExtensionFor =
(
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>,
) =>
(id: string) => {
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
const extensionsState = di.inject(stateInjectable);
const instance = extensionsState.get(id);
if (!instance) {
throw new Error(
`Tried to disable extension with ID "${id}", but it wasn't enabled`,
);
} }
const injectable = getExtension(instance); function disableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>) {
return (extension: FakeExtensionOptions) => {
const extensionsState = di.inject(stateInjectable);
const instance = extensionsState.get(extension.id);
if (!instance) {
throw new Error(`Tried to disable extension with ID "${extension.id}", but it wasn't enabled`);
}
const injectable = di.inject(extensionInjectable, instance);
runInAction(() => {
injectable.deregister(); injectable.deregister();
extensionsState.delete(extension.id);
extensionsState.delete(id);
});
}; };
}

View File

@ -27,7 +27,7 @@ export interface FakeExtensionOptions {
mainOptions?: Partial<LensMainExtension>; mainOptions?: Partial<LensMainExtension>;
} }
export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => { export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionMain({ const instance = new TestExtensionMain({
id, id,
absolutePath: "irrelevant", absolutePath: "irrelevant",
@ -44,7 +44,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
manifestPath: "irrelevant", manifestPath: "irrelevant",
}); });
Object.assign(instance, options); Object.assign(instance, mainOptions);
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = { (instance as Writable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable), fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
@ -56,7 +56,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
return instance; return instance;
}; };
export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => { export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionRenderer({ const instance = new TestExtensionRenderer({
id, id,
absolutePath: "irrelevant", absolutePath: "irrelevant",
@ -73,7 +73,7 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s
manifestPath: "irrelevant", manifestPath: "irrelevant",
}); });
Object.assign(instance, options); Object.assign(instance, rendererOptions);
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = { (instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable), categoryRegistry: di.inject(catalogCategoryRegistryInjectable),