1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-19 05:17:22 +03:00

Support extending KubernetesCluster in extensions (#4702)

* Support extending KubernetesCluster in extensions

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Simplify getItemsByEntityClass

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Make apiVersion string.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Improve entity loading for extension custom types.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Improve comment.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Fix lint.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Properly handle loading custom entity in cluster-frame

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Avoid .bind with .loadOnClusterRenderer

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Fix lint.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Revert style change.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Make loadOnClusterRenderer arrow function again, revert autoInitExtensions change as unnecessary

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Remove commented code.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Document extending KubernetesCluster in extension guides.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti 2022-01-19 14:57:42 +02:00 committed by GitHub
parent 74d92d09d9
commit 79c01daf6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 154 additions and 35 deletions

View File

@ -20,6 +20,7 @@ Each guide or code sample includes the following:
| [Main process extension](main-extension.md) | Main.LensExtension |
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
| [Resource stack (cluster feature)](resource-stack.md) | |
| [Extending KubernetesCluster)](extending-kubernetes-cluster.md) | |
| [Stores](stores.md) | |
| [Components](components.md) | |
| [KubeObjectListLayout](kube-object-list-layout.md) | |

View File

@ -0,0 +1,69 @@
# Extending KubernetesCluster
Extension can specify it's own subclass of Common.Catalog.KubernetesCluster. Extension can also specify a new Category for it in the Catalog.
## Extending Common.Catalog.KubernetesCluster
``` typescript
import { Common } from "@k8slens/extensions";
// The kind must be different from KubernetesCluster's kind
export const kind = "ManagedDevCluster";
export class ManagedDevCluster extends Common.Catalog.KubernetesCluster {
public static readonly kind = kind;
public readonly kind = kind;
}
```
## Extending Common.Catalog.CatalogCategory
These custom Catalog entities can be added a new Category in the Catalog.
``` typescript
import { Common } from "@k8slens/extensions";
import { kind, ManagedDevCluster } from "../entities/ManagedDevCluster";
class ManagedDevClusterCategory extends Common.Catalog.CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Managed Dev Clusters",
icon: ""
};
public spec: Common.Catalog.CatalogCategorySpec = {
group: "entity.k8slens.dev",
versions: [
{
name: "v1alpha1",
entityClass: ManagedDevCluster as any,
},
],
names: {
kind
},
};
}
export { ManagedDevClusterCategory };
export type { ManagedDevClusterCategory as ManagedDevClusterCategoryType };
```
The category needs to be registered in the `onActivate()` method both in main and renderer
``` typescript
// in main's on onActivate
Main.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
```
``` typescript
// in renderer's on onActivate
Renderer.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
```
You can then add the entities to the Catalog as a new source:
``` typescript
this.addCatalogSource("managedDevClusters", this.managedDevClusters);
```

View File

@ -24,6 +24,7 @@ nav:
- Renderer Extension: extensions/guides/renderer-extension.md
- Catalog: extensions/guides/catalog.md
- Resource Stack: extensions/guides/resource-stack.md
- Extending KubernetesCluster: extensions/guides/extending-kubernetes-cluster.md
- Stores: extensions/guides/stores.md
- Working with MobX: extensions/guides/working-with-mobx.md
- Protocol Handlers: extensions/guides/protocol-handlers.md

View File

@ -59,8 +59,8 @@ export interface KubernetesClusterStatus extends CatalogEntityStatus {
}
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public static readonly kind = "KubernetesCluster";
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
public static readonly kind: string = "KubernetesCluster";
public readonly apiVersion = KubernetesCluster.apiVersion;
public readonly kind = KubernetesCluster.kind;

View File

@ -270,11 +270,12 @@ export class ExtensionLoader {
});
};
loadOnClusterRenderer = (entity: KubernetesCluster) => {
loadOnClusterRenderer = (getCluster: () => KubernetesCluster) => {
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
if ((await extension.isEnabledForCluster(entity)) === false) {
// getCluster must be a callback, as the entity might be available only after an extension has been loaded
if ((await extension.isEnabledForCluster(getCluster())) === false) {
return [];
}
@ -299,11 +300,15 @@ export class ExtensionLoader {
});
};
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
const loadingExtensions: ExtensionLoading[] = [];
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>, register: (ext: LensExtension) => Promise<Disposer[]>) {
// Steps of the function:
// 1. require and call .activate for each Extension
// 2. Wait until every extension's onActivate has been resolved
// 3. Call .enable for each extension
// 4. Return ExtensionLoading[]
reaction(() => this.toJSON(), async installedExtensions => {
for (const [extId, extension] of installedExtensions) {
const extensions = [...installedExtensions.entries()]
.map(([extId, extension]) => {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
@ -312,7 +317,8 @@ export class ExtensionLoader {
if (!LensExtensionClass) {
this.nonInstancesByName.add(extension.manifest.name);
continue;
return null;
}
const instance = this.dependencies.createExtensionInstance(
@ -320,27 +326,49 @@ export class ExtensionLoader {
extension,
);
const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
loadingExtensions.push({
return {
extId,
instance,
isBundled: extension.isBundled,
loaded,
});
this.instances.set(extId, instance);
activated: instance.activate(),
};
} catch (err) {
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
}
} else if (!extension.isEnabled && alreadyInit) {
this.removeInstance(extId);
}
}
}, {
fireImmediately: true,
});
return loadingExtensions;
return null;
})
// Remove null values
.filter(extension => Boolean(extension));
// We first need to wait until each extension's `onActivate` is resolved,
// as this might register new catalog categories. Afterwards we can safely .enable the extension.
await Promise.all(extensions.map(extension => extension.activated));
// Return ExtensionLoading[]
return extensions.map(extension => {
const loaded = extension.instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
this.instances.set(extension.extId, extension.instance);
return {
isBundled: extension.isBundled,
loaded,
};
});
}
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
// Setup reaction to load extensions on JSON changes
reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions, register));
// Load initial extensions
return this.loadExtensions(this.toJSON(), register);
}
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {

View File

@ -86,7 +86,6 @@ export class LensExtension {
}
try {
await this.onActivate();
this._isEnabled = true;
this[Disposers].push(...await register(this));
@ -113,6 +112,11 @@ export class LensExtension {
}
}
@action
activate() {
return this.onActivate();
}
protected onActivate(): Promise<void> | void {
return;
}

View File

@ -4,7 +4,7 @@
*/
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor, CatalogEntityKindData } from "../../common/catalog";
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor } from "../../common/catalog";
import { iter } from "../../common/utils";
export class CatalogEntityRegistry {
@ -43,8 +43,8 @@ export class CatalogEntityRegistry {
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
}
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor<T>): T[] {
return this.getItemsForApiKind(apiVersion, kind);
getItemsByEntityClass<T extends CatalogEntity>(constructor: CatalogEntityConstructor<T>): T[] {
return this.items.filter((item) => item instanceof constructor) as T[];
}
}

View File

@ -47,10 +47,25 @@ export class CatalogEntityRegistry {
makeObservable(this);
}
get activeEntity(): CatalogEntity | null {
protected getActiveEntityById() {
return this._entities.get(this.activeEntityId) || null;
}
get activeEntity(): CatalogEntity | null {
const entity = this.getActiveEntityById();
// If the entity was not found but there are rawEntities to be processed,
// try to process them and return the entity.
// This might happen if an extension registered a new Catalog category.
if (this.activeEntityId && !entity && this.rawEntities.length > 0) {
this.processRawEntities();
return this.getActiveEntityById();
}
return entity;
}
set activeEntity(raw: CatalogEntity | string | null) {
if (raw) {
const id = typeof raw === "string"

View File

@ -19,7 +19,7 @@ import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
interface Dependencies {
hostedCluster: Cluster;
loadExtensions: (entity: CatalogEntity) => void;
loadExtensions: (getCluster: () => CatalogEntity) => void;
catalogEntityRegistry: CatalogEntityRegistry;
frameRoutingId: number;
emitEvent: (event: AppEvent) => void;
@ -47,11 +47,12 @@ export const initClusterFrame =
catalogEntityRegistry.activeEntity = hostedCluster.id;
// Only load the extensions once the catalog has been populated
// Only load the extensions once the catalog has been populated.
// Note that the Catalog might still have unprocessed entities until the extensions are fully loaded.
when(
() => Boolean(catalogEntityRegistry.activeEntity),
() => catalogEntityRegistry.items.length > 0,
() =>
loadExtensions(catalogEntityRegistry.activeEntity as KubernetesCluster),
loadExtensions(() => catalogEntityRegistry.activeEntity as KubernetesCluster),
{
timeout: 15_000,
onError: (error) => {

View File

@ -11,15 +11,15 @@ import type { ExtensionLoading } from "../../../../extensions/extension-loader";
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
interface Dependencies {
loadExtensions: () => ExtensionLoading[]
loadExtensions: () => Promise<ExtensionLoading[]>;
// TODO: Move usages of third party library behind abstraction
ipcRenderer: { send: (name: string) => void }
ipcRenderer: { send: (name: string) => void };
// TODO: Remove dependencies being here only for correct timing of initialization
bindProtocolAddRouteHandlers: () => void;
lensProtocolRouterRenderer: { init: () => void };
catalogEntityRegistry: CatalogEntityRegistry
catalogEntityRegistry: CatalogEntityRegistry;
}
const logPrefix = "[ROOT-FRAME]:";
@ -40,7 +40,7 @@ export const initRootFrame =
// maximum time to let bundled extensions finish loading
const timeout = delay(10000);
const loadingExtensions = loadExtensions();
const loadingExtensions = await loadExtensions();
const loadingBundledExtensions = loadingExtensions
.filter((e) => e.isBundled)