diff --git a/docusaurus/docs/extensions/advanced/version-compatibility.md b/docusaurus/docs/extensions/advanced/version-compatibility.md index 4761bd9bcbe..15247216f22 100644 --- a/docusaurus/docs/extensions/advanced/version-compatibility.md +++ b/docusaurus/docs/extensions/advanced/version-compatibility.md @@ -11,9 +11,13 @@ Here are the annotations you can modify: | --- | --- | --- | | `catalog.cattle.io/kube-version` | v2.7.0 | Defines a possible minimum and maximum Kubernetes version for the extension to work with. Prevents extension version from being loaded on the UI | `catalog.cattle.io/rancher-version` | v2.7.0 | Defines a possible minimum and maximum Rancher version for the extension to work with. Prevents extension version from being loaded on the UI +| `catalog.cattle.io/host` | v2.7.0 | Defines the host for the extension, which should have the value `rancher-manager`. Prevents extension version from being loaded on the UI +| `catalog.cattle.io/ui-extensions-version` | v2.9.0 | Defines a possible minimum and maximum Extensions API version for the extension to work with. Prevents extension version from being loaded on the UI | `catalog.cattle.io/ui-version` | v2.7.3 | Defines a possible minimum and maximum Rancher Dashboard version for the extension to work with. Extension version will be loaded but will appear as disabled - -Since both `catalog.cattle.io/kube-version` and `catalog.cattle.io/rancher-version` will prevent a given extension version from being loaded onto the UI by the extensions operator, `catalog.cattle.io/ui-version` will have a sligthly different behaviour and allow for the extension version to be loaded, but will disable it on the Install, Upgrade and Rollback scenarios, where that given version will not appear on those dropdowns and also the extension version button on the side panel will be disabled and will show a tooltip on hover with the information, such as: + +**NOTE: The annotation `catalog.cattle.io/ui-extensions-version` will become mandatory from Rancher 2.10 and onwards. If the annotation is not present on a given extension Helm Chart, the extension itself will not be loaded** + +All annotations will prevent the loading of an extension into Rancher apart from `catalog.cattle.io/ui-version`, which will have a sligthly different behaviour and allow for the extension version to be loaded, but will disable it on the Install, Upgrade and Rollback scenarios, where that given version will not appear on those dropdowns and also the extension version button on the side panel will be disabled and will show a tooltip on hover with the information, such as: ![UI version annotation](../screenshots/ui-version-annotation.png) \ No newline at end of file diff --git a/docusaurus/docs/extensions/support-matrix.md b/docusaurus/docs/extensions/support-matrix.md index 711f28be254..dc900df2b65 100644 --- a/docusaurus/docs/extensions/support-matrix.md +++ b/docusaurus/docs/extensions/support-matrix.md @@ -5,11 +5,12 @@ The Shell package enables Extensions to integrate with Rancher. It's important to know which version of the Shell package is compatible with each Rancher version: -| | Rancher 2.7.x | Rancher 2.8.x | Rancher 2.9.x | -|---|---|---|---| -|Shell **0.3.8**|Supported|Limited support|Not supported| -|Shell 0.5.3/**1.2.3**|Limited support|Supported|Not supported| -|Shell **2.0.1**|Not supported|Not supported|Supported| +| | Rancher 2.7.x | Rancher 2.8.x
(Extensions API V1) | Rancher 2.9.x
(Extensions API V2) | Rancher 2.10.x
(Extensions API V3) | +|---|---|---|---|---| +|Shell **0.3.8**|Supported|Limited support|Not supported|Not supported| +|Shell 0.5.3/**1.2.3**|Limited support|Supported|Not supported|Not supported| +|Shell **2.0.1**|Not supported|Not supported|Supported|Not supported| +|Shell **3.0.0**|Not supported|Not supported|Not supported|Supported| To know more about the Shell package versioning take a look at the diagram [here](./rancher-2.9-support). diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 40b24a7589d..7f9f355fd83 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -4267,6 +4267,7 @@ inactivity: plugins: incompatibleRancherVersion: "The latest version of this extension ({ version }) is not compatible with the current Rancher version ({ required })." incompatibleKubeVersion: "The latest version of this extension ({ version }) is not compatible with the current Kube version ({ required })." + incompatibleUiExtensionsApiVersionMissing: 'The latest version of this extension ({ version }) is missing the mandatory annotation catalog.cattle.io/ui-extensions-version from Rancher 2.10 and onwards.' incompatibleUiExtensionsApiVersion: "The latest version of this extension ({ version }) is not compatible with the current Extensions API version ({ required })." incompatibleHost: 'The latest version of this extension ({ version }) has a host of "{ required }" which is not compatible with this application "{ mainHost }".' currentInstalledVersionBlockedByKubeVersion: "This version is not compatible with the current Kubernetes version ({ kubeVersion } Vs { kubeVersionToCheck })." @@ -4286,9 +4287,11 @@ plugins: title: Error loading extension message: Could not load extension code generic: Extension error - api: This Extension is not compatible with the current Extensions API version - host: This Extension is not compatible with this application - version: This Extension is not compatible with this version of Rancher + apiAnnotationMissing: 'Unable to load Extension. The version installed is not compatible with the current Extensions API Version (Extension is missing the annotation catalog.cattle.io/ui-extensions-version which implies incompatibility with Rancher 2.10 and onwards)' + api: Unable to load Extension. The version installed is not compatible with the current Extensions API version + host: Unable to load Extension. The version installed is not compatible with this application host + version: Unable to load Extension. The version installed is not compatible with the current version of Rancher + kubeVersion: Unable to load Extension. The version installed is not compatible with the current Rancher kubernetes version load: An error occurred loading the code for this Extension developerPkg: This Extension has been loaded internally, so we won't load the external version success: @@ -4308,6 +4311,7 @@ plugins: versionError: Could not load version information requiresRancherVersion: "Requires Rancher {required}" requiresKubeVersion: "Requires Kube version {required}" + requiresExtensionApiVersionMissing: 'Missing the annotation catalog.cattle.io/ui-extensions-version which implies incompatibility with Rancher 2.10 and onwards' requiresExtensionApiVersion: "Requires Extensions API version {required}" requiresHost: 'Requires a host that matches "{mainHost}"' empty: diff --git a/shell/config/uiplugins.js b/shell/config/uiplugins.js index 10c724faa1c..6d6193dfd34 100644 --- a/shell/config/uiplugins.js +++ b/shell/config/uiplugins.js @@ -3,44 +3,36 @@ 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'; -export const UI_PLUGIN_ANNOTATION_VALUE = 'plugins'; - -export const UI_PLUGIN_OPERATOR_CRD_CHART_NAME = 'ui-plugin-operator-crd'; -export const UI_PLUGIN_OPERATOR_CHART_NAME = 'ui-plugin-operator'; - -export const UI_PLUGIN_CHARTS = [ - UI_PLUGIN_OPERATOR_CHART_NAME, - UI_PLUGIN_OPERATOR_CRD_CHART_NAME, -]; - -// Expected chart repo name for the UI Plugins operator -export const UI_PLUGIN_OPERATOR_REPO_NAME = 'rancher-charts'; - -// Info for the Helm Chart Repository that we will add -export const UI_PLUGINS_REPO_NAME = 'rancher-ui-plugins'; - -export const UI_PLUGINS_REPO_URL = 'https://github.com/rancher/ui-plugin-charts'; -export const UI_PLUGINS_REPO_BRANCH = 'main'; - -// Info for the Helm Chart Repo for Partner Extensions -export const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions'; - -export const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions'; -export const UI_PLUGINS_PARTNERS_REPO_BRANCH = 'main'; - -// Info for the Helm Chart Repo for Community Extensions -export const UI_PLUGINS_COMMUNITY_REPO_NAME = 'community-extensions'; +export const UI_PLUGIN_ANNOTATION = { + NAME: 'catalog.cattle.io/ui-component', + VALUE: 'plugins', +}; -export const UI_PLUGINS_COMMUNITY_REPO_URL = 'https://github.com/rancher/community-extensions'; -export const UI_PLUGINS_COMMUNITY_REPO_BRANCH = 'main'; +// 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 = { @@ -57,22 +49,15 @@ export const UI_PLUGIN_CHART_ANNOTATIONS = { 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' -}; - -// Plugin Metadata properties -export const UI_PLUGIN_METADATA = { - RANCHER_VERSION: 'rancherVersion', - EXTENSION_VERSION: 'extVersion', - EXTENSIONS_HOST: 'host', - DISPLAY_NAME: 'displayName', + CATALOG: 'catalog.cattle.io/ui-extensions-catalog', }; export const EXTENSIONS_INCOMPATIBILITY_TYPES = { - UI: 'uiVersion', - EXTENSIONS_API: 'extensionsApiVersion', - KUBE: 'kubeVersion', - HOST: 'host' + UI: 'uiVersion', + EXTENSIONS_API_MISSING: 'extensionsApiVersionMissing', + EXTENSIONS_API: 'extensionsApiVersion', + KUBE: 'kubeVersion', + HOST: 'host', }; export const EXTENSIONS_INCOMPATIBILITY_DATA = { @@ -80,74 +65,89 @@ export const EXTENSIONS_INCOMPATIBILITY_DATA = { type: EXTENSIONS_INCOMPATIBILITY_TYPES.UI, cardMessageKey: 'plugins.incompatibleRancherVersion', tooltipKey: 'plugins.info.requiresRancherVersion', - mainHost: UI_PLUGIN_HOST_APP + }, + 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', - mainHost: UI_PLUGIN_HOST_APP }, KUBE: { type: EXTENSIONS_INCOMPATIBILITY_TYPES.KUBE, cardMessageKey: 'plugins.incompatibleKubeVersion', tooltipKey: 'plugins.info.requiresKubeVersion', - mainHost: UI_PLUGIN_HOST_APP }, HOST: { type: EXTENSIONS_INCOMPATIBILITY_TYPES.HOST, cardMessageKey: 'plugins.incompatibleHost', tooltipKey: 'plugins.info.requiresHost', - mainHost: UI_PLUGIN_HOST_APP - } + mainHost: UI_PLUGIN_HOST_APP, + }, }; export function isUIPlugin(chart) { - return !!chart?.versions.find((v) => { - return v.annotations && v.annotations[UI_PLUGIN_ANNOTATION_NAME] === UI_PLUGIN_ANNOTATION_VALUE; - }); + 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) => { - return v.annotations && v.annotations[name] === value; - }); + return !!chart?.versions.find((v) => v.annotations?.[name] === value); } /** - * Get value of the annotation from teh latest version for a chart + * Get value of the annotation from the latest version for a chart */ export function uiPluginAnnotation(chart, name) { - if (chart?.versions?.length > 0) { - return chart.versions[0].annotations?.[name]; - } - - return undefined; + return chart?.versions?.[0]?.annotations?.[name]; } /** - * Parse the rancher version string + * Parse the Rancher version string */ function parseRancherVersion(v) { - let parsedRancherVersion = semver.coerce(v)?.version; - const splitArr = parsedRancherVersion.split('.'); + 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) { - parsedRancherVersion = `${ splitArr[0] }.${ splitArr[1] }.999`; + parsedVersion = `${ splitArr[0] }.${ splitArr[1] }.999`; } - return parsedRancherVersion; + return parsedVersion; } -// i18n-uses plugins.error.generic, plugins.error.api, plugins.error.host +/** + * 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 - * @returns String || Boolean + * 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, loadedPlugins) { +export function shouldNotLoadPlugin(UIPluginResource, { rancherVersion, kubeVersion }, loadedPlugins) { if (!UIPluginResource.name || !UIPluginResource.version || !UIPluginResource.endpoint) { return 'plugins.error.generic'; } @@ -155,25 +155,37 @@ export function shouldNotLoadPlugin(UIPluginResource, rancherVersion, loadedPlug // 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.spec?.plugin?.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]; + 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 || UI_EXTENSIONS_API_VERSION; + const parsedUiExtensionsApiVersion = semver.coerce(UI_EXTENSIONS_API_VERSION)?.version; const parsedRancherVersion = rancherVersion ? parseRancherVersion(rancherVersion) : ''; + const parsedKubeVersion = kubeVersion ? semver.coerce(kubeVersion)?.version : ''; - if (requiredUiExtensionsVersion && !semver.satisfies(parsedUiExtensionsApiVersion, requiredUiExtensionsVersion)) { + 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_METADATA.EXTENSIONS_HOST]; + 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_METADATA.RANCHER_VERSION]; + const requiredRancherVersion = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION]; if (requiredRancherVersion && !semver.satisfies(parsedRancherVersion, requiredRancherVersion)) { return 'plugins.error.version'; @@ -197,104 +209,62 @@ export function shouldNotLoadPlugin(UIPluginResource, rancherVersion, loadedPlug /** * 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 - * @returns Boolean || Object + * + * 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; - - // semver.coerce will get rid of any suffix on the version numbering (-rc, -head, etc) + const versionObj = { + ...version, isVersionCompatible: true, versionIncompatibilityData: {} + }; const parsedRancherVersion = rancherVersion ? parseRancherVersion(rancherVersion) : ''; - const requiredUiVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.UI_VERSION]; - const requiredKubeVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]; - const versionObj = { ...version }; - - // reset compatibility property - versionObj.isVersionCompatible = true; - versionObj.versionIncompatibilityData = {}; - - // check "catalog.cattle.io/kube-version" annotation - // we keep it as first check since there is a card notification to be displayed - // in case an extension version installed has an incompatibility with the kube version and is not loaded - if (kubeVersion && requiredKubeVersion && !semver.satisfies(kubeVersion, requiredKubeVersion)) { - if (!returnObj) { - return false; + 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; } - - versionObj.isVersionCompatible = false; - versionObj.versionIncompatibilityData = Object.assign({}, EXTENSIONS_INCOMPATIBILITY_DATA.KUBE); - versionObj.versionIncompatibilityData.required = requiredKubeVersion; - - return versionObj; - } - - // we aren't on a "published" version of Rancher and therefore in a "-head" or similar - // Backend will NOT block an extension version from being available IF we are on HEAD versions!! - // we need to enforce that check if we are on a HEAD world - if (rancherVersion && rancherVersion.includes('-')) { - const requiredRancherVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION]; - - if (parsedRancherVersion && !semver.satisfies(parsedRancherVersion, requiredRancherVersion)) { - if (!returnObj) { - return false; - } - - versionObj.isVersionCompatible = false; - versionObj.versionIncompatibilityData = Object.assign({}, EXTENSIONS_INCOMPATIBILITY_DATA.UI); - versionObj.versionIncompatibilityData.required = requiredRancherVersion; - - return versionObj; - } - } - - // check host application - const requiredHost = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_HOST]; - - if (requiredHost && requiredHost !== UI_PLUGIN_HOST_APP) { - if (!returnObj) { - return false; - } - - versionObj.isVersionCompatible = false; - versionObj.versionIncompatibilityData = Object.assign({}, EXTENSIONS_INCOMPATIBILITY_DATA.HOST); - versionObj.versionIncompatibilityData.required = requiredHost; - - return versionObj; } - // check "catalog.cattle.io/ui-extensions-version" annotation - const requiredUiExtensionsApiVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]; - const parsedUiExtensionsApiVersion = semver.coerce(UI_EXTENSIONS_API_VERSION)?.version || UI_EXTENSIONS_API_VERSION; - - if (requiredUiExtensionsApiVersion && parsedUiExtensionsApiVersion && !semver.satisfies(parsedUiExtensionsApiVersion, requiredUiExtensionsApiVersion)) { - if (!returnObj) { - return false; - } - - versionObj.isVersionCompatible = false; - versionObj.versionIncompatibilityData = Object.assign({}, EXTENSIONS_INCOMPATIBILITY_DATA.EXTENSIONS_API); - versionObj.versionIncompatibilityData.required = requiredUiExtensionsApiVersion; - - return versionObj; - } - - // check "catalog.cattle.io/ui-version" annotation - if (requiredUiVersion && parsedRancherVersion && !semver.satisfies(parsedRancherVersion, requiredUiVersion)) { - if (!returnObj) { - return false; - } - - versionObj.isVersionCompatible = false; - versionObj.versionIncompatibilityData = Object.assign({}, EXTENSIONS_INCOMPATIBILITY_DATA.UI); - versionObj.versionIncompatibilityData.required = requiredUiVersion; - - return versionObj; - } - - if (returnObj) { - return versionObj; - } - - return true; + return returnObj ? versionObj : true; } export function isChartVersionHigher(versionA, versionB) { diff --git a/shell/config/version.js b/shell/config/version.js index 15c286cdc53..abbf8856b06 100644 --- a/shell/config/version.js +++ b/shell/config/version.js @@ -2,6 +2,7 @@ * Store version data retrieved from the backend /rancherversion API */ let _versionData = { RancherPrime: 'false' }; +let _kubeVersionData = {}; export function isRancherPrime() { return _versionData.RancherPrime?.toLowerCase() === 'true'; @@ -15,3 +16,12 @@ export function setVersionData(v) { // Remove any properties on 'v' we don't want _versionData = JSON.parse(JSON.stringify(v)); } + +export function getKubeVersionData() { + return _kubeVersionData; +} + +export function setKubeVersionData(v) { + // Remove any properties on 'v' we don't want + _kubeVersionData = JSON.parse(JSON.stringify(v)); +} diff --git a/shell/initialize/install-plugins.js b/shell/initialize/install-plugins.js index 69a11ad4f41..45dfb9508c4 100644 --- a/shell/initialize/install-plugins.js +++ b/shell/initialize/install-plugins.js @@ -25,7 +25,6 @@ import plugins from '@shell/core/plugins.js'; import pluginsLoader from '@shell/core/plugins-loader.js'; import replaceAll from '@shell/plugins/replaceall'; import steveCreateWorker from '@shell/plugins/steve-create-worker'; -import version from '@shell/plugins/version'; import emberCookie from '@shell/plugins/ember-cookie'; import ShortKey from '@shell/plugins/shortkey'; @@ -43,7 +42,7 @@ export async function installPlugins(vueApp) { } export async function installInjectedPlugins(app, vueApp) { - const pluginDefinitions = [config, cookieUniversal, axios, plugins, pluginsLoader, axiosShell, intNumber, codeMirror, nuxtClientInit, replaceAll, backButton, plugin, version, steveCreateWorker, emberCookie]; + const pluginDefinitions = [config, cookieUniversal, axios, plugins, pluginsLoader, axiosShell, intNumber, codeMirror, nuxtClientInit, replaceAll, backButton, plugin, steveCreateWorker, emberCookie]; const installations = pluginDefinitions.map(async(pluginDefinition) => { if (typeof pluginDefinition === 'function') { diff --git a/shell/mixins/resource-manager.js b/shell/mixins/resource-manager.js index c249eff812f..d38b16160f5 100644 --- a/shell/mixins/resource-manager.js +++ b/shell/mixins/resource-manager.js @@ -55,7 +55,7 @@ export default { const type = types[i]; const status = hash[type].status; // if it's namespaced, we get the data on 'items' prop, for non-namespaced it's 'data' prop... - const requestData = hash[type].value.items || hash[type].value.data || hash[type].value; + const requestData = hash[type]?.value?.items || hash[type]?.value?.data || hash[type]?.value; if (status === 'fulfilled' && resourceData.data[type] && resourceData.data[type].applyTo?.length) { for (let y = 0; y < resourceData.data[type].applyTo.length; y++) { diff --git a/shell/pages/c/_cluster/uiplugins/AddExtensionRepos.vue b/shell/pages/c/_cluster/uiplugins/AddExtensionRepos.vue index d0b45848392..5b1f190cfdf 100644 --- a/shell/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +++ b/shell/pages/c/_cluster/uiplugins/AddExtensionRepos.vue @@ -2,14 +2,7 @@ import { CATALOG } from '@shell/config/types'; import Dialog from '@shell/components/Dialog.vue'; import Checkbox from '@components/Form/Checkbox/Checkbox.vue'; -import { - UI_PLUGINS_REPO_NAME, - UI_PLUGINS_REPO_URL, - UI_PLUGINS_REPO_BRANCH, - UI_PLUGINS_PARTNERS_REPO_NAME, - UI_PLUGINS_PARTNERS_REPO_URL, - UI_PLUGINS_PARTNERS_REPO_BRANCH, -} from '@shell/config/uiplugins'; +import { UI_PLUGINS_REPOS } from '@shell/config/uiplugins'; import { isRancherPrime } from '@shell/config/version'; export default { @@ -38,15 +31,15 @@ export default { reposInfo: { official: { repo: undefined, - name: UI_PLUGINS_REPO_NAME, - url: UI_PLUGINS_REPO_URL, - branch: UI_PLUGINS_REPO_BRANCH, + name: UI_PLUGINS_REPOS.OFFICIAL.NAME, + url: UI_PLUGINS_REPOS.OFFICIAL.URL, + branch: UI_PLUGINS_REPOS.OFFICIAL.BRANCH, }, partners: { repo: undefined, - name: UI_PLUGINS_PARTNERS_REPO_NAME, - url: UI_PLUGINS_PARTNERS_REPO_URL, - branch: UI_PLUGINS_PARTNERS_REPO_BRANCH, + name: UI_PLUGINS_REPOS.PARTNERS.NAME, + url: UI_PLUGINS_REPOS.PARTNERS.URL, + branch: UI_PLUGINS_REPOS.PARTNERS.BRANCH, } }, isDialogActive: false, @@ -55,10 +48,10 @@ export default { computed: { hasRancherUIPluginsRepo() { - return !!this.repos.find((r) => r.urlDisplay === UI_PLUGINS_REPO_URL); + return !!this.repos.find((r) => r.urlDisplay === UI_PLUGINS_REPOS.OFFICIAL.URL); }, hasRancherUIPartnersPluginsRepo() { - return !!this.repos.find((r) => r.urlDisplay === UI_PLUGINS_PARTNERS_REPO_URL); + return !!this.repos.find((r) => r.urlDisplay === UI_PLUGINS_REPOS.PARTNERS.URL); } }, diff --git a/shell/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue b/shell/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue index 2c14af25e56..59ae943c1d4 100644 --- a/shell/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +++ b/shell/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue @@ -4,7 +4,7 @@ import AppModal from '@shell/components/AppModal.vue'; import { LabeledInput } from '@components/Form/LabeledInput'; import Checkbox from '@components/Form/Checkbox/Checkbox.vue'; import { UI_PLUGIN } from '@shell/config/types'; -import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins'; +import { UI_PLUGIN_CHART_ANNOTATIONS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins'; export default { emits: ['closed'], @@ -107,8 +107,9 @@ export default { endpoint: url, noCache: true, metadata: { - developer: 'true', - direct: 'true' + developer: 'true', + direct: 'true', + [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>= 3', }, noAuth: true } diff --git a/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue b/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue index fe0f78a3224..9f6598eb97c 100644 --- a/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +++ b/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue @@ -181,12 +181,6 @@ export default {
- { return { @@ -13,8 +10,8 @@ const mockedStore = () => { t: (text: string) => text, 'management/schemaFor': () => true, 'management/findAll': () => [ - { urlDisplay: UI_PLUGINS_REPO_URL }, - { urlDisplay: UI_PLUGINS_PARTNERS_REPO_URL }, + { urlDisplay: UI_PLUGINS_REPOS.OFFICIAL.URL }, + { urlDisplay: UI_PLUGINS_REPOS.PARTNERS.URL }, ] } }; diff --git a/shell/pages/c/_cluster/uiplugins/index.vue b/shell/pages/c/_cluster/uiplugins/index.vue index c910e2aac08..39d778f5129 100644 --- a/shell/pages/c/_cluster/uiplugins/index.vue +++ b/shell/pages/c/_cluster/uiplugins/index.vue @@ -33,12 +33,11 @@ import { isChartVersionHigher, UI_PLUGIN_NAMESPACE, UI_PLUGIN_CHART_ANNOTATIONS, - UI_PLUGINS_REPO_URL, - UI_PLUGINS_PARTNERS_REPO_URL, - UI_PLUGIN_HOST_APP, + UI_PLUGINS_REPOS, EXTENSIONS_INCOMPATIBILITY_TYPES } from '@shell/config/uiplugins'; import TabTitle from '@shell/components/TabTitle'; +import versions from '@shell/utils/versions'; const MAX_DESCRIPTION_LENGTH = 200; @@ -87,7 +86,7 @@ export default { hasFeatureFlag: true, defaultIcon: require('~shell/assets/images/generic-plugin.svg'), reloadRequired: false, - rancherVersion: getVersionData()?.Version, + rancherVersion: null, showCatalogList: false }; }, @@ -103,22 +102,25 @@ export default { } } - hash.load = await this.$store.dispatch('catalog/load', { reset: true }); + hash.load = this.$store.dispatch('catalog/load', { reset: true }); if (this.$store.getters['management/schemaFor'](MANAGEMENT.CLUSTER)) { - hash.localCluster = await this.$store.dispatch('management/find', { type: MANAGEMENT.CLUSTER, id: 'local' }); + hash.localCluster = this.$store.dispatch('management/find', { type: MANAGEMENT.CLUSTER, id: 'local' }); } if (this.$store.getters['management/schemaFor'](CATALOG.OPERATION)) { - hash.helmOps = await this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION }); + hash.helmOps = this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION }); } if (this.$store.getters['management/schemaFor'](CATALOG.CLUSTER_REPO)) { - hash.repos = await this.$store.dispatch('management/findAll', { type: CATALOG.CLUSTER_REPO }, { force: true }); + hash.repos = this.$store.dispatch('management/findAll', { type: CATALOG.CLUSTER_REPO }, { force: true }); } + hash.versions = versions.fetch({ store: this.$store }); + const res = await allHash(hash); + this.rancherVersion = getVersionData()?.Version; this.plugins = res.plugins || []; this.repos = res.repos || []; this.helmOps = res.helmOps || []; @@ -153,8 +155,8 @@ export default { showAddReposBanner() { const hasExtensionReposBannerSetting = this.addExtensionReposBannerSetting?.value === 'true'; - const uiPluginsRepoNotFound = isRancherPrime() && !this.repos?.find((r) => r.urlDisplay === UI_PLUGINS_REPO_URL); - const uiPluginsPartnersRepoNotFound = !this.repos?.find((r) => r.urlDisplay === UI_PLUGINS_PARTNERS_REPO_URL); + const uiPluginsRepoNotFound = isRancherPrime() && !this.repos?.find((r) => r.urlDisplay === UI_PLUGINS_REPOS.OFFICIAL.URL); + const uiPluginsPartnersRepoNotFound = !this.repos?.find((r) => r.urlDisplay === UI_PLUGINS_REPOS.PARTNERS.URL); return hasExtensionReposBannerSetting && (uiPluginsRepoNotFound || uiPluginsPartnersRepoNotFound); }, @@ -286,7 +288,7 @@ export default { switch (latestNotCompatible.versionIncompatibilityData?.type) { case EXTENSIONS_INCOMPATIBILITY_TYPES.HOST: item.incompatibilityMessage = this.t(latestNotCompatible.versionIncompatibilityData?.cardMessageKey, { - version: latestNotCompatible.version, required: latestNotCompatible.versionIncompatibilityData?.required, mainHost: UI_PLUGIN_HOST_APP + version: latestNotCompatible.version, required: latestNotCompatible.versionIncompatibilityData?.required, mainHost: latestNotCompatible.versionIncompatibilityData?.mainHost }, true); break; default: @@ -379,9 +381,9 @@ export default { const error = this.uiErrors[e]; if (error && typeof error === 'string') { - chart.error = this.t(this.uiErrors[e]); + chart.installedError = this.t(this.uiErrors[e]); } else { - chart.error = false; + chart.installedError = ''; } } }); @@ -400,21 +402,6 @@ export default { if (plugin.description && plugin.description.length > MAX_DESCRIPTION_LENGTH) { plugin.description = `${ plugin.description.substr(0, MAX_DESCRIPTION_LENGTH) } ...`; } - - // check if kube version compatibility is met for installed extension - if (plugin.uiplugin) { - const versionInstalled = plugin.uiplugin.spec?.plugin?.version; - const versionInstalledData = plugin.versions.find((v) => v.version === versionInstalled); - - if (versionInstalledData) { - const kubeVersionToCheck = versionInstalledData.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]; - const versionSupportedData = isSupportedChartVersion({ version: versionInstalledData, kubeVersion: this.kubeVersion }); - - if (this.kubeVersion && !versionSupportedData.isVersionCompatible && versionSupportedData.versionIncompatibilityData?.type === EXTENSIONS_INCOMPATIBILITY_TYPES.KUBE) { - plugin.installedError = this.t('plugins.currentInstalledVersionBlockedByKubeVersion', { kubeVersion: this.kubeVersion, kubeVersionToCheck }, true); - } - } - } }); // Sort by name @@ -423,66 +410,72 @@ export default { }, watch: { - helmOps(neu) { + helmOps: { + handler(neu) { // Get Helm operations for UI plugins and order by date - let pluginOps = neu.filter((op) => { - return op.namespace === UI_PLUGIN_NAMESPACE; - }); + let pluginOps = neu.filter((op) => { + return op.namespace === UI_PLUGIN_NAMESPACE; + }); - pluginOps = sortBy(pluginOps, 'metadata.creationTimestamp', true); + pluginOps = sortBy(pluginOps, 'metadata.creationTimestamp', true); - // Go through the installed plugins - (this.available || []).forEach((plugin) => { - const op = pluginOps.find((o) => o.status?.releaseName === plugin.name); + // Go through the installed plugins + (this.available || []).forEach((plugin) => { + const op = pluginOps.find((o) => o.status?.releaseName === plugin.name); - if (op) { - const active = op.metadata.state?.transitioning; - const error = op.metadata.state?.error; + if (op) { + const active = op.metadata.state?.transitioning; + const error = op.metadata.state?.error; - this.errors[plugin.name] = error; + this.errors[plugin.name] = error; - if (active) { + if (active) { // Can use the status directly, apart from upgrade, which maps to install - const status = op.status.action === 'upgrade' ? 'install' : op.status.action; + const status = op.status.action === 'upgrade' ? 'install' : op.status.action; - this.updatePluginInstallStatus(plugin.name, status); - } else if (op.status.action === 'uninstall') { + this.updatePluginInstallStatus(plugin.name, status); + } else if (op.status.action === 'uninstall') { // Uninstall has finished - this.updatePluginInstallStatus(plugin.name, false); - } else if (error) { + this.updatePluginInstallStatus(plugin.name, false); + } else if (error) { + this.updatePluginInstallStatus(plugin.name, false); + } + } else { this.updatePluginInstallStatus(plugin.name, false); } - } else { - this.updatePluginInstallStatus(plugin.name, false); - } - }); + }); + }, + deep: true }, - plugins(neu, old) { - const installed = this.$store.getters['uiplugins/plugins']; - const shouldHaveLoaded = (installed || []).filter((p) => !this.uiErrors[p.name] && !p.builtin); - let changes = 0; + plugins: { + handler(neu) { + const installed = this.$store.getters['uiplugins/plugins']; + const shouldHaveLoaded = (installed || []).filter((p) => !this.uiErrors[p.name] && !p.builtin); + let changes = 0; - // Did the user remove an extension - if (neu?.length < shouldHaveLoaded.length) { - changes++; - } + // Did the user remove an extension + if (neu?.length < shouldHaveLoaded.length) { + changes++; + } - neu.forEach((plugin) => { - const existing = installed.find((p) => !p.removed && p.name === plugin.name && p.version === plugin.version); + neu.forEach((plugin) => { + const existing = installed.find((p) => !p.removed && p.name === plugin.name && p.version === plugin.version); - if (!existing && plugin.isInitialized) { - if (!this.uiErrors[plugin.name]) { - changes++; + if (!existing && plugin.isInitialized) { + if (!this.uiErrors[plugin.name]) { + changes++; + } + + this.updatePluginInstallStatus(plugin.name, false); } + }); - this.updatePluginInstallStatus(plugin.name, false); + if (changes > 0) { + this['reloadRequired'] = true; } - }); - - if (changes > 0) { - this['reloadRequired'] = true; - } + }, + deep: true } }, @@ -852,9 +845,9 @@ export default { > -> {{ plugin.upgrade }}

- + {{ plugin.installedError }}

-
i { + color: var(--error); + height: $error-icon-size; + font-size: $error-icon-size; + width: $error-icon-size; + } } } diff --git a/shell/plugins/plugin.js b/shell/plugins/plugin.js index 523f9e5da2e..67c29206ba0 100644 --- a/shell/plugins/plugin.js +++ b/shell/plugins/plugin.js @@ -1,6 +1,8 @@ // This plugin loads any UI Plugins at app load time import { allHashSettled } from '@shell/utils/promise'; import { shouldNotLoadPlugin, UI_PLUGIN_BASE_URL } from '@shell/config/uiplugins'; +import { getKubeVersionData, getVersionData } from '@shell/config/version'; +import versions from '@shell/utils/versions'; export default async function(context) { if (process.env.excludeOperatorPkg === 'true') { @@ -26,31 +28,37 @@ export default async function(context) { } if (loadPlugins) { - // TODO: Get rancher version using the new API (can't use setting as we have not loading the store) - const rancherVersion = undefined; - // Fetch list of installed plugins from endpoint try { - const res = await context.store.dispatch('management/request', { - url: `${ UI_PLUGIN_BASE_URL }`, - method: 'GET', - headers: { accept: 'application/json' }, - redirectUnauthorized: false, + const res = await allHashSettled({ + versions: versions.fetch(context), + plugins: context.store.dispatch('management/request', { + url: `${ UI_PLUGIN_BASE_URL }`, + method: 'GET', + headers: { accept: 'application/json' }, + redirectUnauthorized: false, + }) }); - if (res) { - const entries = res.entries || res.Entries || {}; + if (res.plugins.status === 'rejected') { + throw new Error(res.reason); + } - Object.values(entries).forEach((plugin) => { - const shouldNotLoad = shouldNotLoadPlugin(plugin, rancherVersion, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean + const kubeVersion = getKubeVersionData()?.gitVersion; + const rancherVersion = getVersionData().Version; - if (!shouldNotLoad) { - hash[plugin.name] = context.$plugin.loadPluginAsync(plugin); - } else { - context.store.dispatch('uiplugins/setError', { name: plugin.name, error: shouldNotLoad }); - } - }); - } + const plugins = res.plugins.value; + const entries = plugins.entries || plugins.Entries || {}; + + Object.values(entries).forEach((plugin) => { + const shouldNotLoad = shouldNotLoadPlugin(plugin, { rancherVersion, kubeVersion }, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean + + if (!shouldNotLoad) { + hash[plugin.name] = context.$plugin.loadPluginAsync(plugin); + } else { + context.store.dispatch('uiplugins/setError', { name: plugin.name, error: shouldNotLoad }); + } + }); } catch (e) { if (e?.code === 404) { // Not found, so extensions operator probably not installed diff --git a/shell/plugins/version.js b/shell/plugins/version.js index d1e51131eec..f3cd4080021 100644 --- a/shell/plugins/version.js +++ b/shell/plugins/version.js @@ -1,21 +1,11 @@ /** - * Fetch version metadata from backend /rancherversion API and store it + * Fetch version metadata from backend /rancherversion and /versionAPI and store it * * This metadata does not change for an installation of Rancher */ -import { setVersionData } from '@shell/config/version'; +import versions from '@shell/utils/versions'; export default async function({ store }) { - try { - const response = await store.dispatch('rancher/request', { - url: '/rancherversion', - method: 'get', - redirectUnauthorized: false - }); - - setVersionData(response); - } catch (e) { - console.warn('Failed to fetch Rancher version metadata', e); // eslint-disable-line no-console - } + await versions.fetch({ store }); } diff --git a/shell/utils/versions.ts b/shell/utils/versions.ts new file mode 100644 index 00000000000..9e33b48b72d --- /dev/null +++ b/shell/utils/versions.ts @@ -0,0 +1,39 @@ +import { setKubeVersionData, setVersionData } from '@shell/config/version'; + +class Versions { + private promise?: Promise; + + async fetch(context: { store: any }): Promise { + if (this.promise) { + return this.promise; + } + + const rancherVersionRequest = context.store.dispatch('rancher/request', { + url: '/rancherversion', + method: 'get', + redirectUnauthorized: false + }).then((response: any) => { + setVersionData(response); + }).catch((e: Error) => { + console.warn('Failed to fetch Rancher version metadata', e); // eslint-disable-line no-console + }); + + const kubeVersionRequest = context.store.dispatch('rancher/request', { + url: '/version', + method: 'get', + redirectUnauthorized: false + }).then((response: any) => { + setKubeVersionData(response); + }).catch((e: Error) => { + console.warn('Failed to fetch Kube version metadata', e); // eslint-disable-line no-console + }); + + this.promise = Promise.allSettled([rancherVersionRequest, kubeVersionRequest]); + + return this.promise; + } +} + +const versions = new Versions(); + +export default versions; diff --git a/shell/vue.config.js b/shell/vue.config.js index c00f4ac238d..a17c7043127 100644 --- a/shell/vue.config.js +++ b/shell/vue.config.js @@ -74,6 +74,7 @@ const getProxyConfig = (proxyConfig) => ({ '/meta': proxyMetaOpts(api), // Browser API UI '/v1-*': proxyOpts(api), // SAML, KDM, etc '/rancherversion': proxyPrimeOpts(api), // Rancher version endpoint + '/version': proxyPrimeOpts(api), // Rancher Kube version endpoint // These are for Ember embedding '/c/*/edit': proxyOpts('https://127.0.0.1:8000'), // Can't proxy all of /c because that's used by Vue too '/k/': proxyOpts('https://127.0.0.1:8000'), @@ -362,6 +363,9 @@ const getVirtualModulesAutoImport = (dir) => { return new VirtualModulesPlugin(autoImportTypes); }; +// Get current shell version +const shellPkgData = require(path.join(__dirname, 'package.json')); + /** * DefinePlugin does string replacement within our code. We may want to consider replacing it with something else. In code we'll see something like * process.env.commit even though process and env aren't even defined objects. This could cause people to be mislead. @@ -377,6 +381,7 @@ const createEnvVariablesPlugin = (routerBasePath, rancherEnv) => new webpack.Def 'process.env.rancherEnv': JSON.stringify(rancherEnv), 'process.env.harvesterPkgUrl': JSON.stringify(process.env.HARVESTER_PKG_URL), 'process.env.api': JSON.stringify(api), + 'process.env.UI_EXTENSIONS_API_VERSION': JSON.stringify(shellPkgData.version), // Store the Router Base as env variable that we can use in `shell/config/router.js` 'process.env.routerBase': JSON.stringify(routerBasePath), __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',