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:
parent
74d92d09d9
commit
79c01daf6a
@ -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) | |
|
||||
|
69
docs/extensions/guides/extending-kubernetes-cluster.md
Normal file
69
docs/extensions/guides/extending-kubernetes-cluster.md
Normal 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);
|
||||
```
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user