Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict installed apps upgrade #13086

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,8 @@ catalog:
label: Resources
busy: The related resources will appear when {app} is fully installed.
values: Values YAML
upgrade:
uncertainUpgradeWarningTooltip: Unable to identify source chart and repository for this application in order to determine if an upgrade is available
chart:
registry:
label: Container Registry
Expand Down
25 changes: 21 additions & 4 deletions shell/list/catalog.cattle.io.app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import PaginatedResourceTable from '@shell/components/PaginatedResourceTable';
import { APP_UPGRADE_STATUS } from '@shell/store/catalog';

export default {
name: 'ListApps',
Expand All @@ -21,6 +22,10 @@ export default {
}
},

data() {
return { APP_UPGRADE_STATUS };
},

async fetch() {
await this.$store.dispatch('catalog/load');
},
Expand All @@ -36,18 +41,30 @@ export default {
>
<template #cell:upgrade="{row}">
<span
v-if="row.upgradeAvailable"
v-if="row.upgradeAvailable === APP_UPGRADE_STATUS.SINGLE_UPGRADE"
class="badge-state bg-warning hand"
@click="row.goToUpgrade(row.upgradeAvailable)"
@click="row.goToUpgrade(row.upgradeAvailableVersion)"
>
{{ row.upgradeAvailable }}
{{ row.upgradeAvailableVersion }}
<i class="icon icon-upload" />
</span>
<span
v-else-if="row.upgradeAvailable === false"
v-else-if="row.upgradeAvailable === APP_UPGRADE_STATUS.NOT_APPLICABLE"
v-t="'catalog.app.managed'"
class="text-muted"
/>
<span
v-else-if="row.upgradeAvailable === APP_UPGRADE_STATUS.NO_UPGRADE"
class="text-muted"
/>
<span
v-else-if="row.upgradeAvailable === APP_UPGRADE_STATUS.MULTIPLE_UPGRADES"
>
<i
v-clean-tooltip="t('catalog.app.upgrade.uncertainUpgradeWarningTooltip')"
class="icon icon-info"
/>
</span>
</template>
</PaginatedResourceTable>
</template>
Expand Down
9 changes: 5 additions & 4 deletions shell/mixins/__tests__/chart.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ChartMixin from '@shell/mixins/chart';
import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
import { mount } from '@vue/test-utils';
import { APP_UPGRADE_STATUS } from '@shell/store/catalog';

describe('chartMixin', () => {
const testCases = {
Expand All @@ -10,10 +11,10 @@ describe('chartMixin', () => {
['any_other_id', 0]
],
managedApps: [
[false, false, 0],
[true, null, 0],
[true, true, 0],
[true, false, 1],
[false, APP_UPGRADE_STATUS.NOT_APPLICABLE, 0],
[true, APP_UPGRADE_STATUS.NO_UPGRADE, 0],
[true, 'some-version', 0],
[true, APP_UPGRADE_STATUS.NOT_APPLICABLE, 1],
],
};

Expand Down
4 changes: 2 additions & 2 deletions shell/mixins/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { formatSi, parseSi } from '@shell/utils/units';
import { CAPI, CATALOG } from '@shell/config/types';
import { isPrerelease } from '@shell/utils/version';
import difference from 'lodash/difference';
import { LINUX } from '@shell/store/catalog';
import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
import { clone } from '@shell/utils/object';
import { merge } from 'lodash';

Expand Down Expand Up @@ -181,7 +181,7 @@ export default {
warnings.unshift(this.t('gatekeeperIndex.deprecated', {}, true));
}

if (this.existing && this.existing.upgradeAvailable === false) {
if (this.existing && this.existing.upgradeAvailable === APP_UPGRADE_STATUS.NOT_APPLICABLE) {
warnings.unshift(this.t('catalog.install.warning.managed', {
name: this.existing.name,
version: this.chart ? this.query.versionName : null
Expand Down
88 changes: 88 additions & 0 deletions shell/models/__tests__/catalog.cattle.io.app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import CatalogApp from '@shell/models/catalog.cattle.io.app';
import { APP_UPGRADE_STATUS } from '@shell/store/catalog';

const latestVersion = '1.16.2';
const secondLatestVersion = '1.16.1';
const chartName = 'cert-manager';

const appCo = {
repoName: 'appCo',
home: 'https://apps.rancher.io/applications/cert-manager'
};

const certManagerFromCli = { home: 'https://github.com/jetstack/cert-manager' };

const certManagerOfficial = {
repoName: 'certManagerOfficial',
home: 'https://cert-manager.io'
};

// cert-manager chart from application collection OCI repo
const matchingChart1 = {
name: chartName,
repoName: appCo.repoName,
versions: [{
version: latestVersion,
home: appCo.home,
repoName: appCo.repoName,
annotations: {}
}]
};

// cert-manager chart from its official helm repo 'https://cert-manager.io' added to Rancher UI repositories
const matchingChart2 = {
name: chartName,
repoName: certManagerOfficial.repoName,
versions: [{
version: latestVersion,
home: certManagerOfficial.home,
repoName: certManagerOfficial.repoName,
annotations: {},
}]
};

const installedChartFromCli = {
metadata: {
name: chartName,
home: certManagerFromCli.home,
version: secondLatestVersion
}
};

const installedChartFromRancherUI = {
metadata: {
name: chartName,
home: certManagerOfficial.home,
version: secondLatestVersion
}
};

// // home is not set
// const installedCustomChartFromRancherUI = {
// metadata: {
// name: chartName,
// version: secondLatestVersion
// }
// };

describe('class CatalogApp', () => {
describe('upgradeAvailable', () => {
// TODO: more test cases
const testCases = [
[installedChartFromCli, [matchingChart1, matchingChart2], APP_UPGRADE_STATUS.NO_UPGRADE],
[installedChartFromRancherUI, [matchingChart1, matchingChart2], APP_UPGRADE_STATUS.SINGLE_UPGRADE]
];

it.each(testCases)('should return the correct upgrade status', (installedChart: Object, matchingCharts: any, expected: any) => {
const catalogApp = new CatalogApp({ spec: { chart: installedChart } }, {
rootGetters: {
'catalog/chart': () => matchingCharts,
currentCluster: { workerOSs: ['linux'] },
'prefs/get': () => false
}
});

expect(catalogApp.upgradeAvailable).toBe(expected);
});
});
});
84 changes: 53 additions & 31 deletions shell/models/catalog.cattle.io.app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SHOW_PRE_RELEASE } from '@shell/store/prefs';
import { set } from '@shell/utils/object';

import SteveModel from '@shell/plugins/steve/steve-class';
import { compatibleVersionsFor } from '@shell/store/catalog';
import { compatibleVersionsFor, APP_UPGRADE_STATUS } from '@shell/store/catalog';

export default class CatalogApp extends SteveModel {
showMasthead(mode) {
Expand All @@ -22,6 +22,8 @@ export default class CatalogApp extends SteveModel {
set(this, 'skipCRDs', false);
set(this, 'timeout', 300);
set(this, 'wait', true);
set(this, 'foundMultipleUpgradeMatches', false);
set(this, 'upgradeAvailableVersion', '');
}

get _availableActions() {
Expand All @@ -40,7 +42,7 @@ export default class CatalogApp extends SteveModel {
}

get warnDeletionMessage() {
if (this.upgradeAvailable === false) {
if (this.upgradeAvailable === APP_UPGRADE_STATUS.NOT_APPLICABLE) {
return this.t('catalog.delete.warning.managed', { name: this.name });
}

Expand All @@ -56,45 +58,66 @@ export default class CatalogApp extends SteveModel {

const chartName = chart.metadata?.name;
const repoName = chart.metadata?.annotations?.[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME] || this.metadata?.labels?.[CATALOG_ANNOTATIONS.CLUSTER_REPO_NAME];
const preferRepoType = chart.metadata?.annotations?.[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE] || 'cluster';

const match = this.$rootGetters['catalog/chart']({
const matchingCharts = this.$rootGetters['catalog/chart']({
chartName,
repoName,
preferRepoType,
includeHidden
});
includeHidden,
multiple: true
}) || [];

if (matchingCharts.length === 0) {
return;
}

// Filtering matches by verifying if the current version is in the matched chart's available versions, and that the home value matches as well
const thisHome = chart?.metadata?.home;
const bestMatches = matchingCharts.filter((m) => m.versions.some((v) => v.version === this.currentVersion && v.home === thisHome));
Copy link
Member Author

@momesgin momesgin Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nwmac as we discussed, I've updated the filtering logic, and it seems to be working well. The only drawback is that it's a bit expensive since it requires iterating through the versions array.


if (bestMatches.length === 0) {
return;
}

if (bestMatches.length === 1) {
return bestMatches[0];
}

return match;
// found multiple matches but couldn't choose one unique match
this.foundMultipleUpgradeMatches = true;
}

get currentVersion() {
return this.spec?.chart?.metadata?.version;
}

get upgradeAvailable() {
// false = does not apply (managed by fleet)
// null = no upgrade found
// object = version available to upgrade to
// one the following statuses gets returned:
// NOT_APPLICABLE - managed by fleet
// NO_UPGRADE - no upgrade found
// SINGLE_UPGRADE - a version available to upgrade to
// MULTIPLE_UPGRADES - more than one match found

if (
this.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.MANAGED] ||
this.spec?.chart?.metadata?.annotations?.[FLEET.BUNDLE_ID]
) {
// Things managed by fleet shouldn't show upgrade available even if there might be.
return false;
return APP_UPGRADE_STATUS.NOT_APPLICABLE;
}
const chart = this.matchingChart(false);

if (this.foundMultipleUpgradeMatches) {
return APP_UPGRADE_STATUS.MULTIPLE_UPGRADES;
}

if ( !chart ) {
return null;
return APP_UPGRADE_STATUS.NO_UPGRADE;
}

const workerOSs = this.$rootGetters['currentCluster'].workerOSs;

const showPreRelease = this.$rootGetters['prefs/get'](SHOW_PRE_RELEASE);

const thisVersion = this.spec?.chart?.metadata?.version;
let versions = chart.versions;

if (!showPreRelease) {
Expand All @@ -106,45 +129,45 @@ export default class CatalogApp extends SteveModel {
const newestChart = versions?.[0];
const newestVersion = newestChart?.version;

if ( !thisVersion || !newestVersion ) {
return null;
if ( !this.currentVersion || !newestVersion ) {
return APP_UPGRADE_STATUS.NO_UPGRADE;
}

if ( compare(thisVersion, newestVersion) < 0 ) {
return cleanupVersion(newestVersion);
if ( compare(this.currentVersion, newestVersion) < 0 ) {
// set the available upgrade version to be used in other places
this.upgradeAvailableVersion = cleanupVersion(newestVersion);

return APP_UPGRADE_STATUS.SINGLE_UPGRADE;
}

return null;
return APP_UPGRADE_STATUS.NO_UPGRADE;
}

get upgradeAvailableSort() {
const version = this.upgradeAvailable;

if ( !version ) {
return '~'; // Tilde sorts after all numbers and letters
if (this.upgradeAvailable === APP_UPGRADE_STATUS.SINGLE_UPGRADE) {
return sortable(this.upgradeAvailableVersion);
}

return sortable(version);
return '~'; // Tilde sorts after all numbers and letters
}

get currentVersionCompatible() {
const workerOSs = this.$rootGetters['currentCluster'].workerOSs;

const chart = this.matchingChart(false);
const thisVersion = this.spec?.chart?.metadata?.version;

if (!chart) {
return true;
}

const versionInChart = chart.versions.find((version) => version.version === thisVersion);
const versionInChart = chart.versions.find((version) => version.version === this.currentVersion);

if (!versionInChart) {
return true;
}
const compatibleVersions = compatibleVersionsFor(chart, workerOSs, true) || [];

const thisVersionCompatible = !!compatibleVersions.find((version) => version.version === thisVersion);
const thisVersionCompatible = !!compatibleVersions.find((version) => version.version === this.currentVersion);

return thisVersionCompatible;
}
Expand All @@ -153,7 +176,7 @@ export default class CatalogApp extends SteveModel {
if (this.currentVersionCompatible) {
return null;
}
if (this.upgradeAvailable) {
if (this.upgradeAvailableVersion) {
return this.t('catalog.os.versionIncompatible');
}

Expand All @@ -162,11 +185,10 @@ export default class CatalogApp extends SteveModel {

goToUpgrade(forceVersion, fromTools) {
const match = this.matchingChart(true);
const versionName = this.spec?.chart?.metadata?.version;
const query = {
[NAMESPACE]: this.metadata.namespace,
[NAME]: this.metadata.name,
[VERSION]: forceVersion || versionName,
[VERSION]: forceVersion || this.currentVersion,
};

if ( match ) {
Expand Down Expand Up @@ -221,7 +243,7 @@ export default class CatalogApp extends SteveModel {
}

get versionDisplay() {
return cleanupVersion(this.spec?.chart?.metadata?.version);
return cleanupVersion(this.currentVersion);
}

get versionSort() {
Expand Down
4 changes: 2 additions & 2 deletions shell/pages/c/_cluster/explorer/tools/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ export default {
>{{ t('catalog.charts.deploysOnWindows') }}</label>
</div>
<div class="version">
<template v-if="opt.app && opt.app.upgradeAvailable">
v{{ opt.app.currentVersion }} <b><i class="icon icon-chevron-right" /> v{{ opt.app.upgradeAvailable }}</b>
<template v-if="opt.app && opt.app.upgradeAvailableVersion">
v{{ opt.app.currentVersion }} <b><i class="icon icon-chevron-right" /> v{{ opt.app.upgradeAvailableVersion }}</b>
</template>
<template v-else-if="opt.app">
v{{ opt.app.currentVersion }}
Expand Down
Loading
Loading