diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index 6fbb6d14659..396e00f6f74 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -142,6 +142,20 @@ export default class ExtensionsPagePo extends PagePo { return this.extensionCard(extensionName).getId(`extension-card-uninstall-btn-${ extensionName }`).click(); } + extensionCardErrorTooltip(extensionName: string): Cypress.Chainable { + return this.extensionCard(extensionName).getId(`extension-card-error-${ extensionName }`); + } + + extensionCardErrorTooltipContent(extensionName: string): Cypress.Chainable { + return this.extensionCardErrorTooltip(extensionName).trigger('mouseenter').then(() => { + return cy.get('.v-popper__popper.v-popper--theme-tooltip .v-popper__inner'); + }); + } + + extensionCardInstallButton(extensionName: string): Cypress.Chainable { + return this.extensionCard(extensionName).getId(`extension-card-install-btn-${ extensionName }`); + } + // ------------------ extension install modal ------------------ extensionInstallModal() { return this.self().get('[data-modal="installPluginDialog"]'); @@ -205,6 +219,10 @@ export default class ExtensionsPagePo extends PagePo { return this.extensionDetails().getId('extension-details-close').click(); } + extensionDetailsErrorBanner(): Cypress.Chainable { + return this.extensionDetails().getId('extension-details-error'); + } + // ------------------ extension tabs ------------------ extensionTabInstalledClick(): Cypress.Chainable { return this.extensionTabs.clickNthTab(1); diff --git a/cypress/e2e/tests/pages/extensions/extensions.spec.ts b/cypress/e2e/tests/pages/extensions/extensions.spec.ts index a3b7958259b..27cee02f86a 100644 --- a/cypress/e2e/tests/pages/extensions/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions/extensions.spec.ts @@ -441,4 +441,162 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionCardClick(DISABLED_CACHE_EXTENSION_NAME); extensionsPo.extensionDetailsTitle().should('contain', DISABLED_CACHE_EXTENSION_NAME); }); + + it.skip('[Vue3 Skip]: Should display error for installed extension without required API version annotation', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples'); + + // Intercept the request fetching chart data to modify the 'clock' extension + cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos*', (req) => { + req.continue((res) => { + if (res.body && res.body.data) { + res.body.data.forEach((chart) => { + if (chart.metadata.name === 'clock') { + // Remove the required API annotation to simulate the missing condition + delete chart.versions[0].annotations['catalog.cattle.io/ui-extensions-version']; + } + }); + } + }); + }); + + extensionsPo.goTo(); + extensionsPo.extensionTabAvailableClick(); + + extensionsPo.extensionCardInstallClick('clock'); + extensionsPo.extensionInstallModal().should('be.visible'); + extensionsPo.installModalInstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + + // Verify that the 'clock' extension card displays an error icon with tooltip + extensionsPo.extensionCardErrorTooltip('clock').should('exist'); + extensionsPo.extensionCardErrorTooltipContent('clock').should('contain', 'This Extension is not compatible with the current Extensions API version'); + + extensionsPo.extensionCardClick('clock'); + + // Verify that the extension details display an error banner with the correct message + extensionsPo.extensionDetailsErrorBanner().should('contain', 'This Extension is not compatible with the current Extensions API version'); + + extensionsPo.extensionDetailsCloseClick(); + + extensionsPo.extensionCardUninstallClick('clock'); + extensionsPo.extensionUninstallModal().should('be.visible'); + extensionsPo.uninstallModaluninstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + extensionsPo.extensionCard('clock').should('not.exist'); + }); + + it.skip('[Vue3 Skip]: Should display error for installed extension with outdated required API version', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples'); + + // Intercept the request fetching chart data to modify the 'clock' extension + cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos*', (req) => { + req.continue((res) => { + if (res.body && res.body.data) { + res.body.data.forEach((chart) => { + if (chart.metadata.name === 'clock') { + // Set the required API version to a value less than UI_EXTENSIONS_API_VERSION + chart.versions[0].annotations['catalog.cattle.io/ui-extensions-version'] = '>=2.0.0'; + } + }); + } + }); + }); + + extensionsPo.goTo(); + extensionsPo.extensionTabAvailableClick(); + + extensionsPo.extensionCardInstallClick('clock'); + extensionsPo.extensionInstallModal().should('be.visible'); + extensionsPo.installModalInstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + + // Verify that the 'clock' extension card displays an error icon with tooltip + extensionsPo.extensionCardErrorTooltip('clock').should('exist'); + extensionsPo.extensionCardErrorTooltipContent('clock').should('contain', 'This Extension is not compatible with the current Extensions API version'); + + extensionsPo.extensionCardClick('clock'); + + // Verify that the extension details display an error banner with the correct message + extensionsPo.extensionDetailsErrorBanner().should('contain', 'This Extension is not compatible with the current Extensions API version'); + + extensionsPo.extensionDetailsCloseClick(); + + extensionsPo.extensionCardUninstallClick('clock'); + extensionsPo.extensionUninstallModal().should('be.visible'); + extensionsPo.uninstallModaluninstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + extensionsPo.extensionCard('clock').should('not.exist'); + }); + + it.skip('[Vue3 Skip]: Should successfully install extension with satisfying required API version', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples'); + + // Intercept the request fetching chart data to modify the 'clock' extension + cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos*', (req) => { + req.continue((res) => { + if (res.body && res.body.data) { + res.body.data.forEach((chart) => { + if (chart.metadata.name === 'clock') { + // Set the required API version to a value that satisfies the UI version + chart.versions[0].annotations['catalog.cattle.io/ui-extensions-version'] = '>=3.0.0'; + } + }); + } + }); + }); + + extensionsPo.goTo(); + extensionsPo.extensionTabAvailableClick(); + + extensionsPo.extensionCardInstallClick('clock'); + extensionsPo.extensionInstallModal().should('be.visible'); + extensionsPo.installModalInstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + + // Verify that the 'clock' extension card does not display an error icon + extensionsPo.extensionCardErrorTooltip('clock').should('not.exist'); + + extensionsPo.extensionCardClick('clock'); + + // Verify that the extension details do not display an error banner + extensionsPo.extensionDetailsErrorBanner().should('not.exist'); + + extensionsPo.extensionDetailsCloseClick(); + + extensionsPo.extensionCardUninstallClick('clock'); + extensionsPo.extensionUninstallModal().should('be.visible'); + extensionsPo.uninstallModaluninstallClick(); + + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + extensionsPo.extensionTabInstalledClick(); + extensionsPo.extensionCard('clock').should('not.exist'); + }); }); diff --git a/shell/config/uiplugins.js b/shell/config/uiplugins.js index aeb332e82a3..46f919ccf24 100644 --- a/shell/config/uiplugins.js +++ b/shell/config/uiplugins.js @@ -91,6 +91,13 @@ export function uiPluginAnnotation(chart, name) { return undefined; } +/** + * Coerce the UI_EXTENSIONS_API_VERSION to remove any pre-release identifiers + */ +export function coerceCurrentExtensionsVersion() { + return semver.coerce(UI_EXTENSIONS_API_VERSION)?.version; +} + // i18n-uses plugins.error.generic, plugins.error.api, plugins.error.host // Should we load a plugin, based on the metadata returned by the backend? @@ -104,8 +111,12 @@ export function shouldNotLoadPlugin(plugin, rancherVersion, loadedPlugins) { // we are propagating the annotations in pkg/package.json for any extension // inside the "spec.plugin.metadata" property of UIPlugin resource const requiredAPI = plugin.spec?.plugin?.metadata?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]; + const currentVersion = coerceCurrentExtensionsVersion(); - if (requiredAPI && !semver.satisfies(UI_EXTENSIONS_API_VERSION, requiredAPI)) { + // If an extension does not contain the `catalog.cattle.io/ui-extensions-version` + // annotation, it is considered as a legacy extension. + // Otherwise, the ui-extensions-version is compared with the current shell version. + if (!requiredAPI || !semver.satisfies(currentVersion, requiredAPI)) { return 'plugins.error.api'; } @@ -145,8 +156,9 @@ export function isSupportedChartVersion(versionsData) { // Plugin specified a required extension API version const requiredAPI = version.annotations?.[UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]; + const currentVersion = coerceCurrentExtensionsVersion(); - if (requiredAPI && !semver.satisfies(UI_EXTENSIONS_API_VERSION, requiredAPI)) { + if (requiredAPI && !semver.satisfies(currentVersion, requiredAPI)) { return false; } diff --git a/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue b/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue index 9219e478e1b..74e0c895cc2 100644 --- a/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +++ b/shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue @@ -189,6 +189,7 @@ export default { v-if="info.error" color="error" :label="info.error" + data-testid="extension-details-error" class="mt-10" /> diff --git a/shell/vue.config.js b/shell/vue.config.js index c00f4ac238d..3f5f9acd774 100644 --- a/shell/vue.config.js +++ b/shell/vue.config.js @@ -362,6 +362,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 +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',