Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions: Add hooks to support virtual clusters #11064

Merged
merged 23 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -215,7 +228,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 @@ -284,6 +297,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 @@ -317,8 +337,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
Loading