Skip to content

Commit

Permalink
Extensions: Add hooks to support virtual clusters (#11064)
Browse files Browse the repository at this point in the history
* Add hooks to support virtual clusters

* Fix lint issues

* Refinements

* Update for Vue 3 changes

* Fix import

* Minor tweaks

* Fix bug causing e2e tests to fail

* Fix lint issue

* Rename internal properties and ensure they don't break clone/save

* Ensure we generate types for the plugins package to give us access to mapDriver

* Simpler approach

* Fix lint issues and add type

* Remove unused code

* Revery unnecessary changes

* Bug fix for finding model extension

* Factor out string constant and add provider display method

* Add experimental to API

* Update typegen.sh to use SHELL_DIR var

* Move type def

* Fix lint issue

* Address PR feedback

* Update steve-class.js so we always get an array

* Fix type definition
  • Loading branch information
nwmac authored Jan 31, 2025
1 parent 763840d commit 8001967
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 11 deletions.
27 changes: 27 additions & 0 deletions shell/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ import {
LocationConfig,
ExtensionPoint,
TabLocation,
ModelExtensionConstructor,
PluginRouteRecordRaw, RegisterStore, UnregisterStore, CoreStoreSpecifics, CoreStoreConfig, OnNavToPackage, OnNavAwayFromPackage, OnLogOut
} from './types';
import coreStore, { coreStoreModule, coreStoreState } from '@shell/plugins/dashboard-store';
import { defineAsyncComponent, markRaw, Component } from 'vue';

// Registration IDs used for different extension points in the extensions catalog
export const EXT_IDS = {
MODELS: 'models',
MODEL_EXTENSION: 'model-extension',
};

export type ProductFunction = (plugin: IPlugin, store: any) => void;

export class Plugin implements IPlugin {
public id: string;
public name: string;
public types: any = {};
public l10n: { [key: string]: Function[] } = {};
public modelExtensions: { [key: string]: Function[] } = {};
public locales: { locale: string, label: string}[] = [];
public products: ProductFunction[] = [];
public productNames: string[] = [];
Expand Down Expand Up @@ -186,6 +194,17 @@ export class Plugin implements IPlugin {
this._addUIConfig(ExtensionPoint.CARD, where, when, this._createAsyncComponent(card));
}

/**
* Adds a model extension
* @experimental May change or be removed in the future
*
* @param type Model type
* @param clz Class for the model extension (constructor)
*/
addModelExtension(type: string, clz: ModelExtensionConstructor): void {
this.register(EXT_IDS.MODEL_EXTENSION, type, clz);
}

/**
* Wraps a component from an extensionConfig with defineAsyncComponent and
* markRaw. This prepares the component to be loaded dynamically and prevents
Expand Down Expand Up @@ -317,10 +336,18 @@ export class Plugin implements IPlugin {
}

this.l10n[name].push(fn);

// Accumulate model extensions
} else if (type === EXT_IDS.MODEL_EXTENSION) {
if (!this.modelExtensions[name]) {
this.modelExtensions[name] = [];
}
this.modelExtensions[name].push(fn);
} else {
if (!this.types[type]) {
this.types[type] = {};
}

this.types[type][name] = fn;
}
}
Expand Down
32 changes: 26 additions & 6 deletions shell/core/plugins.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { productsLoaded } from '@shell/store/type-map';
import { clearModelCache } from '@shell/plugins/dashboard-store/model-loader';
import { Plugin } from './plugin';
import { EXT_IDS, Plugin } from './plugin';
import { PluginRoutes } from './plugin-routes';
import { UI_PLUGIN_BASE_URL } from '@shell/config/uiplugins';
import { ExtensionPoint } from './types';

const MODEL_TYPE = 'models';

export default function(context, inject, vueApp) {
const {
app, store, $axios, redirect
Expand All @@ -26,6 +24,21 @@ export default function(context, inject, vueApp) {
uiConfig[ExtensionPoint[ep]] = {};
}

/**
* When an extension adds a model extension, it provides the class - we will instantiate that class and store and use that
*/
function instantiateModelExtension($plugin, clz) {
const context = {
dispatch: store.dispatch,
getters: store.getters,
t: store.getters['i18n/t'],
$axios,
$plugin,
};

return new clz(context);
}

inject(
'plugin',
{
Expand Down Expand Up @@ -186,7 +199,7 @@ export default function(context, inject, vueApp) {
Object.keys(plugin.types[typ]).forEach((name) => {
this.unregister(typ, name);

if (typ === MODEL_TYPE) {
if (typ === EXT_IDS.MODELS) {
clearModelCache(name);
}
});
Expand Down Expand Up @@ -255,6 +268,13 @@ export default function(context, inject, vueApp) {
});
});

// Model extensions
Object.keys(plugin.modelExtensions).forEach((name) => {
plugin.modelExtensions[name].forEach((fn) => {
this.register(EXT_IDS.MODEL_EXTENSION, name, instantiateModelExtension(this, fn));
});
});

// Initialize the product if the store is ready
if (productsLoaded()) {
this.loadProducts([plugin]);
Expand Down Expand Up @@ -288,8 +308,8 @@ export default function(context, inject, vueApp) {
dynamic[type] = {};
}

// Accumulate l10n resources rather than replace
if (type === 'l10n') {
// Accumulate l10n resources and model extensions rather than replace
if (type === 'l10n' || type === EXT_IDS.MODEL_EXTENSION) {
if (!dynamic[type][name]) {
dynamic[type][name] = [];
}
Expand Down
93 changes: 91 additions & 2 deletions shell/core/types-provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@ export type ClusterSaveHook = (cluster: any) => Promise<any>
*/
export type RegisterClusterSaveHook = (hook: ClusterSaveHook, name: string, priority?: number, fnContext?: any) => void;

export type ClusterDetailTabs = {
/**
* RKE2 machine pool tabs
*/
machines: boolean,
/**
* RKE2 provisioning logs
*/
logs: boolean,
/**
* RKE2 registration commands
*/
registration: boolean,
/**
* RKE2 snapshots
*/
snapshots: boolean,
/**
* Kube resources related to the instance of provisioning.cattle.io.cluster
*/
related: boolean,
/**
* Kube events associated with the instance of provisioning.cattle.io.cluster
*/
events: boolean,
/**
* Kube conditions of the provisioning.cattle.io.cluster instance
*/
conditions: boolean
};

/**
* Params used when constructing an instance of the cluster provisioner
*/
Expand Down Expand Up @@ -57,7 +88,6 @@ export interface ClusterProvisionerContext {
* The majority of these hooks are used in shell/edit/provisioning.cattle.io.cluster/rke2.vue
*/
export interface IClusterProvisioner {

/**
* Unique ID of the Cluster Provisioner
* If this overlaps with the name of an existing provisioner (seen in the type query param while creating a cluster) this provisioner will overwrite the built-in ui
Expand Down Expand Up @@ -233,7 +263,7 @@ export interface IClusterProvisioner {
registerSaveHooks?(registerBeforeHook: RegisterClusterSaveHook, registerAfterHook: RegisterClusterSaveHook, cluster: any): void;

/**
* Optionally override the save of the cluster resource itself.
* Optionally override the save of the cluster resource itself
*
* https://github.com/rancher/dashboard/blob/master/shell/mixins/create-edit-view/impl.js#L179
*
Expand Down Expand Up @@ -263,3 +293,62 @@ export interface IClusterProvisioner {
*/
provision?(cluster: any, pools: any[]): Promise<any[]>;
}

/**
* Interface that a model extension for the provisioning cluster model should implement
*/
export interface IClusterModelExtension {
/**
* Indicates if this extension should be used for the given cluster
*
* This allows the extension to determine if it should be used for a cluster based on attributes/metadata of its choosing
*
* @param cluster The cluster model (`provisioning.cattle.io.cluster`)
* @returns Whether to use this provisioner for the given cluster.
*/
useFor(cluster: any): boolean;

/**
* Optionally Process the available actions for a cluster and return a (possibly modified) set of actions
*
* @param cluster The cluster model (`provisioning.cattle.io.cluster`)
* @returns List of actions for the cluster or undefined if the list is modified in-place
*/
availableActions?(cluster: any, actions: any[]): any[] | undefined;

/**
* Get the display name for the machine provider for this model
*
* @param cluster The cluster model (`provisioning.cattle.io.cluster`)
* @returns Machine provider display name
*/
machineProviderDisplay?(cluster: any): string;

/**
* Get the display name for the provisioner for this model
*
* @param cluster The cluster model (`provisioning.cattle.io.cluster`)
* @returns Provisioner display name
*/
provisionerDisplay?(cluster: any): string;

/**
* Get the parent cluster for this cluster, or undefined if no parent cluster
*
* @param cluster The cluster model (`provisioning.cattle.io.cluster`)
* @returns ID of the parent cluster
*/
parentCluster?(cluster: any): string;

/**
* Function to run after the cluster has been deleted
*
* @param cluster The cluster (`provisioning.cattle.io.cluster`)
*/
postDelete?(cluster: any): void;

/**
* Existing tabs to show or hide in the cluster's detail view
*/
get detailTabs(): ClusterDetailTabs;
}
40 changes: 40 additions & 0 deletions shell/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,37 @@ export interface DSLReturnType {
// weightType: (input, weight, forBasic)
}

/**
* Context for the constructor of a model extension
*/
export type ModelExtensionContext = {
/**
* Dispatch vuex actions
*/
dispatch: any,
/**
* Get from vuex store
*/
getters: any,
/**
* Used to make http requests
*/
axios: any,
/**
* Definition of the extension
*/
$plugin: any,
/**
* Function to retrieve a localised string
*/
t: (key: string) => string,
};

/**
* Constructor signature for a model extension
*/
export type ModelExtensionConstructor = (context: ModelExtensionContext) => Object;

/**
* Interface for a Dashboard plugin
*/
Expand Down Expand Up @@ -584,6 +615,15 @@ export interface IPlugin {
onLogOut?: OnLogOut
): void;

/**
* Adds a model extension
* @experimental May change or be removed in the future
*
* @param type Model type
* @param clz Class for the model extension (constructor)
*/
addModelExtension(type: string, clz: ModelExtensionConstructor): void;

/**
* Register 'something' that can be dynamically loaded - e.g. model, edit, create, list, i18n
* @param {String} type type of thing to register, e.g. 'edit'
Expand Down
13 changes: 12 additions & 1 deletion shell/detail/provisioning.cattle.io.cluster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default {
async fetch() {
await this.value.waitForProvisioner();
// Support for the 'provisioner' extension
const extClass = this.$plugin.getDynamic('provisioner', this.value.machineProvider);
if (extClass) {
Expand All @@ -94,13 +95,23 @@ export default {
$plugin: this.$store.app.$plugin,
$t: this.t
});
this.extDetailTabs = {
...this.extDetailTabs,
...this.extProvider.detailTabs
};
this.extCustomParams = { provider: this.value.machineProvider };
}
// Support for a model extension
if (this.value.customProvisionerHelper) {
this.extDetailTabs = {
...this.extDetailTabs,
...this.value.customProvisionerHelper.detailTabs
};
this.extCustomParams = { provider: this.value.machineProvider };
}
const schema = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
const fetchOne = { schemaDefinitions: schema.fetchResourceFields() };
Expand Down Expand Up @@ -379,7 +390,7 @@ export default {
},
showNodes() {
return !this.showMachines && this.haveNodes && !!this.nodes.length;
return !this.showMachines && this.haveNodes && !!this.nodes.length && this.extDetailTabs.machines;
},
showSnapshots() {
Expand Down
Loading

0 comments on commit 8001967

Please sign in to comment.