diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 31a5e2b1651..69cdb1530e0 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -4285,9 +4285,10 @@ 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 + api: Loading error! The version installed is not compatible with the current Extensions API version + host: Loading error! The version installed is not compatible with this application host + version: Loading error! The version installed is not compatible with the current version of Rancher + kubeVersion: Loading error! 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: diff --git a/shell/config/uiplugins.js b/shell/config/uiplugins.js index 10c724faa1c..f671688a1e1 100644 --- a/shell/config/uiplugins.js +++ b/shell/config/uiplugins.js @@ -147,7 +147,7 @@ function parseRancherVersion(v) { * Whether an extension should be loaded based on the metadata returned by the backend in the UIPlugins resource instance * @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,10 +155,11 @@ 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] || '>= 3.0.0'; // 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 parsedRancherVersion = rancherVersion ? parseRancherVersion(rancherVersion) : ''; + const parsedKubeVersion = kubeVersion ? semver.coerce(kubeVersion)?.version : ''; if (requiredUiExtensionsVersion && !semver.satisfies(parsedUiExtensionsApiVersion, requiredUiExtensionsVersion)) { return 'plugins.error.api'; @@ -171,6 +172,15 @@ export function shouldNotLoadPlugin(UIPluginResource, rancherVersion, loadedPlug return 'plugins.error.host'; } + // Kube version + if (parsedKubeVersion) { + const requiredKubeVersion = UIPluginResource.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]; + + if (requiredKubeVersion && !semver.satisfies(parsedRancherVersion, requiredKubeVersion)) { + return 'plugins.error.kubeVersion'; + } + } + // Rancher version if (parsedRancherVersion) { const requiredRancherVersion = UIPluginResource.metadata?.[UI_PLUGIN_METADATA.RANCHER_VERSION]; @@ -262,7 +272,7 @@ export function isSupportedChartVersion(versionData, returnObj = false) { } // check "catalog.cattle.io/ui-extensions-version" annotation - const requiredUiExtensionsApiVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]; + const requiredUiExtensionsApiVersion = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION] || '>= 3.0.0'; const parsedUiExtensionsApiVersion = semver.coerce(UI_EXTENSIONS_API_VERSION)?.version || UI_EXTENSIONS_API_VERSION; if (requiredUiExtensionsApiVersion && parsedUiExtensionsApiVersion && !semver.satisfies(parsedUiExtensionsApiVersion, requiredUiExtensionsApiVersion)) { 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/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 {
- 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 +408,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 +843,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..e427ce30d05 100644 --- a/shell/plugins/plugin.js +++ b/shell/plugins/plugin.js @@ -1,6 +1,7 @@ // 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 { setVersionData, setKubeVersionData } from '@shell/config/version'; export default async function(context) { if (process.env.excludeOperatorPkg === 'true') { @@ -26,38 +27,73 @@ 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; + let rancherVersion; + let kubeVersion; + + const reqs = []; + + // Fetch rancher version metadata + reqs.push(context.store.dispatch('rancher/request', { + url: '/rancherversion', + method: 'get', + redirectUnauthorized: false + })); + + // Fetch kubernetes version metadata + reqs.push(context.store.dispatch('rancher/request', { + url: '/version', + method: 'get', + redirectUnauthorized: false + })); // 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, - }); + reqs.push(context.store.dispatch('management/request', { + url: `${ UI_PLUGIN_BASE_URL }`, + method: 'GET', + headers: { accept: 'application/json' }, + redirectUnauthorized: false, + })); + + const response = await Promise.allSettled(reqs); - if (res) { - const entries = res.entries || res.Entries || {}; + if (response[0]?.status === 'rejected') { + console.warn('Failed to fetch Rancher version metadata', response[0]?.reason?.message); // eslint-disable-line no-console + } - Object.values(entries).forEach((plugin) => { - const shouldNotLoad = shouldNotLoadPlugin(plugin, rancherVersion, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean + if (response[1]?.status === 'rejected') { + console.warn('Failed to fetch Kube version metadata', response[1]?.reason?.message); // eslint-disable-line no-console + } - 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 - console.log('Could not load UI Extensions list (Extensions Operator may not be installed)'); // eslint-disable-line no-console - } else { - console.error('Could not load UI Extensions list', e); // eslint-disable-line no-console - } + if (response[2]?.status === 'rejected') { + console.warn('Could not load UI Extensions list', response[2]?.reason?.message); // eslint-disable-line no-console + } + + if (response[0]?.status === 'fulfilled') { + rancherVersion = response[0]?.value?.Version; + setVersionData(response[0]?.value); + } + + if (response[1]?.status === 'fulfilled') { + kubeVersion = response[1]?.value?.gitVersion; + setKubeVersionData(response[1]?.value); + } + + if (response[2]?.status === 'fulfilled' && response[2]?.value) { + const entries = response[2]?.value.entries || response[2]?.value.Entries || {}; + + Object.values(entries).forEach((plugin) => { + let shouldNotLoad = shouldNotLoadPlugin(plugin, { rancherVersion, kubeVersion }, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean + + if (plugin.name === 'elemental') { + shouldNotLoad = 'ssss'; + } + + if (!shouldNotLoad) { + hash[plugin.name] = context.$plugin.loadPluginAsync(plugin); + } else { + context.store.dispatch('uiplugins/setError', { name: plugin.name, error: shouldNotLoad }); + } + }); } // Load all of the plugins diff --git a/shell/plugins/version.js b/shell/plugins/version.js deleted file mode 100644 index d1e51131eec..00000000000 --- a/shell/plugins/version.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Fetch version metadata from backend /rancherversion API and store it - * - * This metadata does not change for an installation of Rancher - */ - -import { setVersionData } from '@shell/config/version'; - -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 - } -} diff --git a/shell/vue.config.js b/shell/vue.config.js index c00f4ac238d..624c3b287fe 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,10 @@ const getVirtualModulesAutoImport = (dir) => { return new VirtualModulesPlugin(autoImportTypes); }; +// Get current shell version +const shellPkgRawData = fs.readFileSync(path.join(__dirname, 'package.json')); +const shellPkgData = JSON.parse(shellPkgRawData); + /** * 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 +382,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',