diff --git a/shell/components/SelectIconGrid.vue b/shell/components/SelectIconGrid.vue index 5ffcd6c8e3..1ffec12084 100644 --- a/shell/components/SelectIconGrid.vue +++ b/shell/components/SelectIconGrid.vue @@ -131,16 +131,6 @@ export default { class="side-label" :class="{'indicator': true }" /> -
- -
-
- -
0 && difference(OSs, permittedSystems).length > 0) { nue.disabled = true; diff --git a/shell/pages/c/_cluster/apps/charts/install.vue b/shell/pages/c/_cluster/apps/charts/install.vue index f75e1c1a2c..7d7b92993a 100644 --- a/shell/pages/c/_cluster/apps/charts/install.vue +++ b/shell/pages/c/_cluster/apps/charts/install.vue @@ -41,7 +41,7 @@ import { import { ignoreVariables } from './install.helpers'; import { findBy, insertAt } from '@shell/utils/array'; import { saferDump } from '@shell/utils/create-yaml'; -import { LINUX, WINDOWS } from '@shell/store/catalog'; +import { WINDOWS, isRancherRepo, getPermittedOSs } from '@shell/store/catalog'; import { SETTING } from '@shell/config/settings'; import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret.vue'; import { generateRandomAlphaString } from '@shell/utils/string'; @@ -766,14 +766,17 @@ export default { }, windowsIncompatible() { - if (this.chart?.windowsIncompatible) { - return this.t('catalog.charts.windowsIncompatible'); - } if (this.versionInfo) { - const incompatibleVersion = !(this.versionInfo?.chart?.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || LINUX).includes('windows'); + const isRancher = isRancherRepo(this.repo, this.chart); + const permittedSystems = getPermittedOSs(this.versionInfo?.chart?.annotations, isRancher); + const incompatibleVersion = permittedSystems.length > 0 && !permittedSystems.includes('windows'); + + if (incompatibleVersion) { + if (!this.chart?.windowsIncompatible) { + return this.t('catalog.charts.versionWindowsIncompatible'); + } - if (incompatibleVersion && !this.chart.windowsIncompatible) { - return this.t('catalog.charts.versionWindowsIncompatible'); + return this.t('catalog.charts.windowsIncompatible'); } } diff --git a/shell/store/__tests__/catalog.test.ts b/shell/store/__tests__/catalog.test.ts index 6070fe17d9..38edebdd7a 100644 --- a/shell/store/__tests__/catalog.test.ts +++ b/shell/store/__tests__/catalog.test.ts @@ -1,7 +1,7 @@ import { CATALOG } from '@shell/config/types'; import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations'; import { - state, getters, actions, mutations, filterAndArrangeCharts + state, getters, actions, mutations, filterAndArrangeCharts, isRancherRepo, compatibleVersionsFor, getPermittedOSs } from '../catalog'; import { createStore } from 'vuex'; @@ -509,4 +509,118 @@ describe('catalog', () => { }); }); }); + + describe('isRancherRepo', () => { + it('should return true if chart.isRancherRepo is true', () => { + const chart = { isRancherRepo: true } as any; + + expect(isRancherRepo(null, chart)).toBe(true); + }); + + it('should return true if repo.isRancherSource is true', () => { + const repo = { isRancherSource: true } as any; + + expect(isRancherRepo(repo, null)).toBe(true); + }); + + it('should return false if repo is not a rancher source', () => { + const repo = { isRancherSource: false } as any; + + expect(isRancherRepo(repo, null)).toBe(false); + }); + }); + + describe('compatibleVersionsFor', () => { + it('should allow versions if no OS constraint is provided', () => { + const chart = { versions: [{ version: '1.0.0' }] } as any; + const versions = compatibleVersionsFor(chart, undefined); + + expect(versions).toHaveLength(1); + }); + + it('should allow windows nodes if permitted-os includes windows', () => { + const chart = { + versions: [{ + version: '1.0.0', + annotations: { 'catalog.cattle.io/permits-os': 'linux,windows' } + }] + } as any; + const versions = compatibleVersionsFor(chart, 'windows'); + + expect(versions).toHaveLength(1); + }); + + it('should block windows nodes if permitted-os does not include windows', () => { + const chart = { + versions: [{ + version: '1.0.0', + annotations: { 'catalog.cattle.io/permits-os': 'linux' } + }] + } as any; + const versions = compatibleVersionsFor(chart, 'windows'); + + expect(versions).toHaveLength(0); + }); + + it('should fallback to LINUX for rancher repos and block windows nodes', () => { + const chart = { isRancherRepo: true, versions: [{ version: '1.0.0' }] } as any; + const versions = compatibleVersionsFor(chart, 'windows'); + + expect(versions).toHaveLength(0); + }); + + it('should not fallback to LINUX for non-rancher repos and allow windows nodes', () => { + const chart = { isRancherRepo: false, versions: [{ version: '1.0.0' }] } as any; + const versions = compatibleVersionsFor(chart, 'windows'); + + expect(versions).toHaveLength(1); + }); + }); + + describe('getPermittedOSs', () => { + it('should return explicitly permitted OSs when the annotation is present on a Rancher repo', () => { + const annotations = { [CATALOG_ANNOTATIONS.PERMITTED_OS]: 'linux,windows' }; + const result = getPermittedOSs(annotations, true); + + expect(result).toHaveLength(2); + expect(result).toContain('linux'); + expect(result).toContain('windows'); + }); + + it('should return explicitly permitted OSs when the annotation is present on a non-Rancher repo', () => { + const annotations = { [CATALOG_ANNOTATIONS.PERMITTED_OS]: 'linux' }; + const result = getPermittedOSs(annotations, false); + + expect(result).toHaveLength(1); + expect(result).toContain('linux'); + }); + + it('should fallback to linux if the annotation is missing on a Rancher repo', () => { + const annotations = {}; + const result = getPermittedOSs(annotations, true); + + expect(result).toHaveLength(1); + expect(result).toContain('linux'); + }); + + it('should return an empty array (no restrictions) if the annotation is missing on a non-Rancher repo', () => { + const annotations = {}; + const result = getPermittedOSs(annotations, false); + + expect(result).toHaveLength(0); + }); + + it('should handle undefined annotations safely for Rancher repos', () => { + const result = getPermittedOSs(undefined, true); + + expect(result).toHaveLength(1); + expect(result).toContain('linux'); + }); + + it('should handle undefined annotations safely for non-Rancher repos', () => { + const result = getPermittedOSs(undefined, false); + + expect(result).toHaveLength(0); + }); + }); }); diff --git a/shell/store/catalog.js b/shell/store/catalog.js index 0147cd64fa..a93904a592 100644 --- a/shell/store/catalog.js +++ b/shell/store/catalog.js @@ -559,7 +559,10 @@ function addChart(ctx, map, chart, repo) { const primeOnly = chart.annotations?.[CATALOG_ANNOTATIONS.PRIME_ONLY] === 'true'; const experimental = !!chart.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL]; - const windowsIncompatible = !(chart.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || '').includes('windows'); + + const isRancherRepoFlag = isRancherRepo(repo, chart); + const permittedSystems = getPermittedOSs(chart.annotations, isRancherRepoFlag); + const windowsIncompatible = permittedSystems.length > 0 && !permittedSystems.includes('windows'); const deploysOnWindows = (chart.annotations?.[CATALOG_ANNOTATIONS.DEPLOYED_OS] || '').includes('windows'); const tags = []; @@ -608,6 +611,7 @@ function addChart(ctx, map, chart, repo) { provides: [], windowsIncompatible, deploysOnWindows, + isRancherRepo: isRancherRepoFlag, tags }); @@ -692,13 +696,13 @@ export function compatibleVersionsFor(chart, os, includePrerelease = true) { } return versions.filter((ver) => { - const osPermitted = (ver?.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || LINUX).split(','); + const osPermitted = getPermittedOSs(ver?.annotations, chart?.isRancherRepo); if ( !includePrerelease && isPrerelease(ver.version) ) { return false; } - if ( !os || difference(os, osPermitted).length === 0) { + if ( !os || osPermitted.length === 0 || difference(os, osPermitted).length === 0) { return true; } @@ -787,3 +791,23 @@ export function filterAndArrangeCharts(charts, { return sortBy(out, ['certifiedSort', 'repoName', 'chartNameDisplay']); } + +/** + * Detects if a repository is a Rancher repository. + */ +export function isRancherRepo(repo, chart) { + return !!(chart?.isRancherRepo || repo?.isRancherSource); +} + +/** + * Returns an array of permitted operating systems for a given chart or version. + * If the chart explicitly defines permitted OSs via annotation, those are returned. + * Otherwise, if the chart is from a Rancher repository, it defaults to Linux. + * External charts with no annotations have no OS restrictions (returns empty array). + */ +export function getPermittedOSs(annotations, isRancher) { + const permittedOs = annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS]; + const fallbackOs = isRancher ? LINUX : ''; + + return (permittedOs || fallbackOs).split(',').filter(Boolean); +}