mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00: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 |
|
| [Main process extension](main-extension.md) | Main.LensExtension |
|
||||||
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
|
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
|
||||||
| [Resource stack (cluster feature)](resource-stack.md) | |
|
| [Resource stack (cluster feature)](resource-stack.md) | |
|
||||||
|
| [Extending KubernetesCluster)](extending-kubernetes-cluster.md) | |
|
||||||
| [Stores](stores.md) | |
|
| [Stores](stores.md) | |
|
||||||
| [Components](components.md) | |
|
| [Components](components.md) | |
|
||||||
| [KubeObjectListLayout](kube-object-list-layout.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
|
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||||
- Catalog: extensions/guides/catalog.md
|
- Catalog: extensions/guides/catalog.md
|
||||||
- Resource Stack: extensions/guides/resource-stack.md
|
- Resource Stack: extensions/guides/resource-stack.md
|
||||||
|
- Extending KubernetesCluster: extensions/guides/extending-kubernetes-cluster.md
|
||||||
- Stores: extensions/guides/stores.md
|
- Stores: extensions/guides/stores.md
|
||||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||||
- Protocol Handlers: extensions/guides/protocol-handlers.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> {
|
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
|
||||||
public static readonly kind = "KubernetesCluster";
|
public static readonly kind: string = "KubernetesCluster";
|
||||||
|
|
||||||
public readonly apiVersion = KubernetesCluster.apiVersion;
|
public readonly apiVersion = KubernetesCluster.apiVersion;
|
||||||
public readonly kind = KubernetesCluster.kind;
|
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)`);
|
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||||
|
|
||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,11 +300,15 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>, register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||||
const loadingExtensions: ExtensionLoading[] = [];
|
// 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 => {
|
const extensions = [...installedExtensions.entries()]
|
||||||
for (const [extId, extension] of installedExtensions) {
|
.map(([extId, extension]) => {
|
||||||
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||||
|
|
||||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||||
@ -312,7 +317,8 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
this.nonInstancesByName.add(extension.manifest.name);
|
this.nonInstancesByName.add(extension.manifest.name);
|
||||||
continue;
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = this.dependencies.createExtensionInstance(
|
const instance = this.dependencies.createExtensionInstance(
|
||||||
@ -320,27 +326,49 @@ export class ExtensionLoader {
|
|||||||
extension,
|
extension,
|
||||||
);
|
);
|
||||||
|
|
||||||
const loaded = instance.enable(register).catch((err) => {
|
return {
|
||||||
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
extId,
|
||||||
});
|
instance,
|
||||||
|
|
||||||
loadingExtensions.push({
|
|
||||||
isBundled: extension.isBundled,
|
isBundled: extension.isBundled,
|
||||||
loaded,
|
activated: instance.activate(),
|
||||||
});
|
};
|
||||||
this.instances.set(extId, instance);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
||||||
}
|
}
|
||||||
} else if (!extension.isEnabled && alreadyInit) {
|
} else if (!extension.isEnabled && alreadyInit) {
|
||||||
this.removeInstance(extId);
|
this.removeInstance(extId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, {
|
return null;
|
||||||
fireImmediately: true,
|
})
|
||||||
|
// 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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
return loadingExtensions;
|
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 {
|
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
||||||
|
|||||||
@ -86,7 +86,6 @@ export class LensExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.onActivate();
|
|
||||||
this._isEnabled = true;
|
this._isEnabled = true;
|
||||||
|
|
||||||
this[Disposers].push(...await register(this));
|
this[Disposers].push(...await register(this));
|
||||||
@ -113,6 +112,11 @@ export class LensExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
activate() {
|
||||||
|
return this.onActivate();
|
||||||
|
}
|
||||||
|
|
||||||
protected onActivate(): Promise<void> | void {
|
protected onActivate(): Promise<void> | void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
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";
|
import { iter } from "../../common/utils";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@ -43,8 +43,8 @@ export class CatalogEntityRegistry {
|
|||||||
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor<T>): T[] {
|
getItemsByEntityClass<T extends CatalogEntity>(constructor: CatalogEntityConstructor<T>): T[] {
|
||||||
return this.getItemsForApiKind(apiVersion, kind);
|
return this.items.filter((item) => item instanceof constructor) as T[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,10 +47,25 @@ export class CatalogEntityRegistry {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeEntity(): CatalogEntity | null {
|
protected getActiveEntityById() {
|
||||||
return this._entities.get(this.activeEntityId) || null;
|
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) {
|
set activeEntity(raw: CatalogEntity | string | null) {
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const id = typeof raw === "string"
|
const id = typeof raw === "string"
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
|||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
hostedCluster: Cluster;
|
hostedCluster: Cluster;
|
||||||
loadExtensions: (entity: CatalogEntity) => void;
|
loadExtensions: (getCluster: () => CatalogEntity) => void;
|
||||||
catalogEntityRegistry: CatalogEntityRegistry;
|
catalogEntityRegistry: CatalogEntityRegistry;
|
||||||
frameRoutingId: number;
|
frameRoutingId: number;
|
||||||
emitEvent: (event: AppEvent) => void;
|
emitEvent: (event: AppEvent) => void;
|
||||||
@ -47,11 +47,12 @@ export const initClusterFrame =
|
|||||||
|
|
||||||
catalogEntityRegistry.activeEntity = hostedCluster.id;
|
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(
|
when(
|
||||||
() => Boolean(catalogEntityRegistry.activeEntity),
|
() => catalogEntityRegistry.items.length > 0,
|
||||||
() =>
|
() =>
|
||||||
loadExtensions(catalogEntityRegistry.activeEntity as KubernetesCluster),
|
loadExtensions(() => catalogEntityRegistry.activeEntity as KubernetesCluster),
|
||||||
{
|
{
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@ -11,15 +11,15 @@ import type { ExtensionLoading } from "../../../../extensions/extension-loader";
|
|||||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
loadExtensions: () => ExtensionLoading[]
|
loadExtensions: () => Promise<ExtensionLoading[]>;
|
||||||
|
|
||||||
// TODO: Move usages of third party library behind abstraction
|
// 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
|
// TODO: Remove dependencies being here only for correct timing of initialization
|
||||||
bindProtocolAddRouteHandlers: () => void;
|
bindProtocolAddRouteHandlers: () => void;
|
||||||
lensProtocolRouterRenderer: { init: () => void };
|
lensProtocolRouterRenderer: { init: () => void };
|
||||||
catalogEntityRegistry: CatalogEntityRegistry
|
catalogEntityRegistry: CatalogEntityRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logPrefix = "[ROOT-FRAME]:";
|
const logPrefix = "[ROOT-FRAME]:";
|
||||||
@ -40,7 +40,7 @@ export const initRootFrame =
|
|||||||
// maximum time to let bundled extensions finish loading
|
// maximum time to let bundled extensions finish loading
|
||||||
const timeout = delay(10000);
|
const timeout = delay(10000);
|
||||||
|
|
||||||
const loadingExtensions = loadExtensions();
|
const loadingExtensions = await loadExtensions();
|
||||||
|
|
||||||
const loadingBundledExtensions = loadingExtensions
|
const loadingBundledExtensions = loadingExtensions
|
||||||
.filter((e) => e.isBundled)
|
.filter((e) => e.isBundled)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user