dashboard/shell/config/uiplugins.js

273 lines
10 KiB
JavaScript

import semver from 'semver';
// Version of the plugin API supported
// here we inject the current shell version that we read in vue.config
export const UI_EXTENSIONS_API_VERSION = process.env.UI_EXTENSIONS_API_VERSION;
export const UI_PLUGIN_HOST_APP = 'rancher-manager';
export const UI_PLUGIN_BASE_URL = '/v1/uiplugins';
export const UI_PLUGIN_NAMESPACE = 'cattle-ui-plugin-system';
// Annotation name and value that indicate a chart is a UI plugin
export const UI_PLUGIN_ANNOTATION = {
NAME: 'catalog.cattle.io/ui-component',
VALUE: 'plugins',
};
// Info for the Helm Chart Repositories
export const UI_PLUGINS_REPOS = {
OFFICIAL: {
NAME: 'rancher-ui-plugins',
URL: 'https://github.com/rancher/ui-plugin-charts',
BRANCH: 'main',
},
PARTNERS: {
NAME: 'partner-extensions',
URL: 'https://github.com/rancher/partner-extensions',
BRANCH: 'main',
},
COMMUNITY: {
NAME: 'community-extensions',
URL: 'https://github.com/rancher/community-extensions',
BRANCH: 'main',
},
};
// Chart annotations
export const UI_PLUGIN_CHART_ANNOTATIONS = {
KUBE_VERSION: 'catalog.cattle.io/kube-version',
RANCHER_VERSION: 'catalog.cattle.io/rancher-version',
EXTENSIONS_VERSION: 'catalog.cattle.io/ui-extensions-version',
UI_VERSION: 'catalog.cattle.io/ui-version',
EXTENSIONS_HOST: 'catalog.cattle.io/ui-extensions-host',
DISPLAY_NAME: 'catalog.cattle.io/display-name',
HIDDEN_BUILTIN: 'catalog.cattle.io/ui-hidden-builtin',
};
// Extension catalog labels
export const UI_PLUGIN_LABELS = {
CATALOG_IMAGE: 'catalog.cattle.io/ui-extensions-catalog-image',
REPOSITORY: 'catalog.cattle.io/ui-extensions-repository',
CATALOG: 'catalog.cattle.io/ui-extensions-catalog',
};
export const EXTENSIONS_INCOMPATIBILITY_TYPES = {
UI: 'uiVersion',
EXTENSIONS_API_MISSING: 'extensionsApiVersionMissing',
EXTENSIONS_API: 'extensionsApiVersion',
KUBE: 'kubeVersion',
HOST: 'host',
};
export const EXTENSIONS_INCOMPATIBILITY_DATA = {
UI: {
type: EXTENSIONS_INCOMPATIBILITY_TYPES.UI,
cardMessageKey: 'plugins.incompatibleRancherVersion',
tooltipKey: 'plugins.info.requiresRancherVersion',
},
EXTENSIONS_API_MISSING: {
type: EXTENSIONS_INCOMPATIBILITY_TYPES.EXTENSIONS_API_MISSING,
cardMessageKey: 'plugins.incompatibleUiExtensionsApiVersionMissing',
tooltipKey: 'plugins.info.requiresExtensionApiVersionMissing',
},
EXTENSIONS_API: {
type: EXTENSIONS_INCOMPATIBILITY_TYPES.EXTENSIONS_API,
cardMessageKey: 'plugins.incompatibleUiExtensionsApiVersion',
tooltipKey: 'plugins.info.requiresExtensionApiVersion',
},
KUBE: {
type: EXTENSIONS_INCOMPATIBILITY_TYPES.KUBE,
cardMessageKey: 'plugins.incompatibleKubeVersion',
tooltipKey: 'plugins.info.requiresKubeVersion',
},
HOST: {
type: EXTENSIONS_INCOMPATIBILITY_TYPES.HOST,
cardMessageKey: 'plugins.incompatibleHost',
tooltipKey: 'plugins.info.requiresHost',
mainHost: UI_PLUGIN_HOST_APP,
},
};
export function isUIPlugin(chart) {
return !!chart?.versions.find((v) => v.annotations?.[UI_PLUGIN_ANNOTATION.NAME] === UI_PLUGIN_ANNOTATION.VALUE);
}
export function uiPluginHasAnnotation(chart, name, value) {
return !!chart?.versions.find((v) => v.annotations?.[name] === value);
}
/**
* Get value of the annotation from the latest version for a chart
*/
export function uiPluginAnnotation(chart, name) {
return chart?.versions?.[0]?.annotations?.[name];
}
/**
* Parse the Rancher version string
*/
function parseRancherVersion(v) {
let parsedVersion = semver.coerce(v)?.version;
const splitArr = parsedVersion?.split('.');
// this is a scenario where we are on a "head" version of some sort... we can't infer the patch version from it
// so we apply a big patch version number to make sure we follow through with the minor
if (v.includes('-') && splitArr?.length === 3) {
parsedVersion = `${ splitArr[0] }.${ splitArr[1] }.999`;
}
return parsedVersion;
}
/**
* Check if a version is incompatible with the current environment
*/
function checkIncompatibility(currentVersion, requiredVersion, incompatibilityData, returnObj, versionObj) {
if ((incompatibilityData.type === EXTENSIONS_INCOMPATIBILITY_TYPES.EXTENSIONS_API_MISSING && !requiredVersion) || (requiredVersion && !semver.satisfies(currentVersion, requiredVersion))) {
if (!returnObj) {
return false;
}
versionObj.isVersionCompatible = false;
versionObj.versionIncompatibilityData = { ...incompatibilityData, required: requiredVersion };
return versionObj;
}
return true;
}
// i18n-uses plugins.error.generic, plugins.error.api, plugins.error.host, plugins.error.kubeVersion, plugins.error.version, plugins.error.developerPkg, plugins.error.apiAnnotationMissing
/**
* Whether an extension should be loaded based on the metadata returned by the backend in the UIPlugins resource instance
* The output will be used to PREVENT loading of an extension that is already installed but isn't compatible with the system
*
* String output will display a message on the extension card to notify users on why the extension was not loaded
*
* @returns String | Boolean
*/
export function shouldNotLoadPlugin(UIPluginResource, { rancherVersion, kubeVersion }, loadedPlugins) {
if (!UIPluginResource.name || !UIPluginResource.version || !UIPluginResource.endpoint) {
return 'plugins.error.generic';
}
// Extension chart specified a required extension API version
// we are propagating the annotations in pkg/package.json for any extension
// inside the "spec.plugin.metadata" property of UIPlugin resource
const requiredUiExtensionsVersion = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION];
// semver.coerce will get rid of any suffix on the version numbering (-rc, -head, etc)
const parsedUiExtensionsApiVersion = semver.coerce(UI_EXTENSIONS_API_VERSION)?.version;
const parsedRancherVersion = rancherVersion ? parseRancherVersion(rancherVersion) : '';
const parsedKubeVersion = kubeVersion ? semver.coerce(kubeVersion)?.version : '';
if (!requiredUiExtensionsVersion) {
return 'plugins.error.apiAnnotationMissing';
} else if (requiredUiExtensionsVersion && !semver.satisfies(parsedUiExtensionsApiVersion, requiredUiExtensionsVersion)) {
return 'plugins.error.api';
}
// Host application
const requiredHost = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_HOST];
if (requiredHost && requiredHost !== UI_PLUGIN_HOST_APP) {
return 'plugins.error.host';
}
// Kube version
if (parsedKubeVersion) {
const requiredKubeVersion = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION];
if (requiredKubeVersion && !semver.satisfies(parsedKubeVersion, requiredKubeVersion)) {
return 'plugins.error.kubeVersion';
}
}
// Rancher version
if (parsedRancherVersion) {
const requiredRancherVersion = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION];
if (requiredRancherVersion && !semver.satisfies(parsedRancherVersion, requiredRancherVersion)) {
return 'plugins.error.version';
}
}
// check if a builtin extension has been loaded before - improve developer experience
const checkLoaded = loadedPlugins.find((p) => p?.name === UIPluginResource?.name);
if (checkLoaded && checkLoaded.builtin) {
return 'plugins.error.developerPkg';
}
if (UIPluginResource.metadata?.[UI_PLUGIN_LABELS.CATALOG]) {
return true;
}
return false;
}
/**
* Wether an extension version is available to be installed, based on the annotations present in the Helm chart version
* backend may not automatically "limit" a particular version but dashboard will disable that version for install with this check
*
* The output will be used to display a message on the extension card to notify users if a LATEST version of an extension is available but isn't compatible (cardMessageKey)
* The output will also disable the buttons in the slide-in panel with extension details, displaying a tooltip message with the reason (tooltipKey)
*
* @returns Boolean | Object
*/
export function isSupportedChartVersion(versionData, returnObj = false) {
const { version, rancherVersion, kubeVersion } = versionData;
const versionObj = {
...version, isVersionCompatible: true, versionIncompatibilityData: {}
};
const parsedRancherVersion = rancherVersion ? parseRancherVersion(rancherVersion) : '';
const parsedUiExtensionsApiVersion = semver.coerce(UI_EXTENSIONS_API_VERSION)?.version;
const checks = [
{
currentVersion: kubeVersion,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.KUBE,
},
{
currentVersion: parsedRancherVersion,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.UI,
},
{
currentVersion: parsedRancherVersion,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.UI_VERSION],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.UI,
},
{
currentVersion: parsedUiExtensionsApiVersion,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.EXTENSIONS_API_MISSING,
},
{
currentVersion: parsedUiExtensionsApiVersion,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.EXTENSIONS_API,
},
{
currentVersion: UI_PLUGIN_HOST_APP,
requiredVersion: version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_HOST],
incompatibilityData: EXTENSIONS_INCOMPATIBILITY_DATA.HOST,
},
];
for (const { currentVersion, requiredVersion, incompatibilityData } of checks) {
const result = checkIncompatibility(currentVersion, requiredVersion, incompatibilityData, returnObj, versionObj);
if (result !== true) {
return result;
}
}
return returnObj ? versionObj : true;
}
export function isChartVersionHigher(versionA, versionB) {
return semver.gt(versionA, versionB);
}