dashboard/shell/mixins/chart.js

596 lines
20 KiB
JavaScript

import { mapGetters } from 'vuex';
import {
REPO_TYPE, REPO, CHART, VERSION, NAMESPACE, NAME, DESCRIPTION as DESCRIPTION_QUERY, DEPRECATED as DEPRECATED_QUERY, HIDDEN, _FLAGGED, _CREATE, _EDIT
} from '@shell/config/query-params';
import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
import { SHOW_PRE_RELEASE, mapPref } from '@shell/store/prefs';
import { NAME as EXPLORER } from '@shell/config/product/explorer';
import { NAME as MANAGER } from '@shell/config/product/manager';
import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
import { formatSi, parseSi } from '@shell/utils/units';
import { CAPI, CATALOG } from '@shell/config/types';
import { isPrerelease, compare } from '@shell/utils/version';
import difference from 'lodash/difference';
import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
import { clone } from '@shell/utils/object';
import { merge } from 'lodash';
export default {
data() {
return {
version: null,
versionInfo: null,
versionInfoError: null,
existing: null,
ignoreWarning: false,
chart: null,
};
},
computed: {
...mapGetters(['currentCluster', 'isRancher']),
showPreRelease: mapPref(SHOW_PRE_RELEASE),
repo() {
return this.$store.getters['catalog/repo']({
repoType: this.query.repoType,
repoName: this.query.repoName,
});
},
showReadme() {
return !!this.versionInfo?.readme;
},
hasReadme() {
return !!this.versionInfo?.appReadme || !!this.versionInfo?.readme;
},
mappedVersions() {
const versions = this.chart?.versions || [];
const selectedVersion = this.targetVersion;
const OSs = this.currentCluster?.workerOSs;
const out = [];
versions.forEach((version) => {
const nue = {
label: version.version,
version: version.version,
originalVersion: version.version,
id: version.version,
created: version.created,
disabled: false,
keywords: version.keywords
};
const permittedSystems = (version?.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || LINUX).split(',');
if (permittedSystems.length > 0 && difference(OSs, permittedSystems).length > 0) {
nue.disabled = true;
}
// if only one OS is allowed, show '<OS>-only' on hover
if (permittedSystems.length === 1) {
nue.label = this.t(`catalog.install.versions.${ permittedSystems[0] }`, { ver: version.version });
}
if (!this.showPreRelease && isPrerelease(version.version)) {
return;
}
out.push(nue);
});
const selectedMatch = out.find((v) => v.id === selectedVersion);
if (!selectedMatch) {
out.unshift({
label: selectedVersion,
originalVersion: selectedVersion,
id: selectedVersion,
created: null,
disabled: false,
keywords: []
});
}
const currentVersion = out.find((v) => v.originalVersion === this.currentVersion);
if (currentVersion) {
currentVersion.label = this.t('catalog.install.versions.current', { ver: this.currentVersion });
}
return out;
},
// Conditionally filter out prerelease versions of the chart.
filteredVersions() {
return this.showPreRelease ? this.mappedVersions : this.mappedVersions.filter((v) => !v.isPre);
},
query() {
const query = this.$route.query;
return {
repoType: query[REPO_TYPE],
repoName: query[REPO],
chartName: query[CHART],
versionName: query[VERSION],
appNamespace: query[NAMESPACE] || '',
appName: query[NAME] || '',
description: query[DESCRIPTION_QUERY],
hidden: query[HIDDEN],
deprecated: query[DEPRECATED_QUERY]
};
},
showDeprecated() {
return this.query.deprecated === 'true' || this.query.deprecated === _FLAGGED;
},
showHidden() {
return this.query.hidden === _FLAGGED;
},
// If the user is installing the app for the first time,
// warn them about CPU and memory requirements.
warnings() {
const warnings = [];
if ( this.existing ) {
// Ignore the limits on upgrade (or if asked by query) and don't show any warnings
} else {
// The UI will show warnings about CPU and memory if
// these annotations are added to Helm chart:
// - catalog.cattle.io/requests-cpu
// - catalog.cattle.io/requests-memory
const needCpu = parseSi(this.version?.annotations?.[CATALOG_ANNOTATIONS.REQUESTS_CPU] || '0');
const needMemory = parseSi(this.version?.annotations?.[CATALOG_ANNOTATIONS.REQUESTS_MEMORY] || '0');
// Note: These are null if unknown
const availableCpu = this.currentCluster?.availableCpu;
const availableMemory = this.currentCluster?.availableMemory;
if ( availableCpu !== null && availableCpu < needCpu ) {
warnings.push(this.t('catalog.install.error.insufficientCpu', {
need: Math.round(needCpu * 100) / 100,
have: Math.round(availableCpu * 100) / 100,
}));
}
if ( availableMemory !== null && availableMemory < needMemory ) {
warnings.push(this.t('catalog.install.error.insufficientMemory', {
need: formatSi(needMemory, {
increment: 1024, suffix: 'iB', firstSuffix: 'B'
}),
have: formatSi(availableMemory, {
increment: 1024, suffix: 'iB', firstSuffix: 'B'
}),
}));
}
}
if (this.chart?.id === OPA_GATE_KEEPER_ID) {
warnings.unshift(this.t('gatekeeperIndex.deprecated', {}, true));
}
if (this.existing && this.existing.upgradeAvailable === APP_UPGRADE_STATUS.NOT_APPLICABLE) {
const manager = this.existing?.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.MANAGED] || 'Rancher';
warnings.unshift(this.t('catalog.install.warning.managed', {
name: this.existing.name,
version: this.chart ? this.query.versionName : null,
manager: manager === 'true' ? 'Rancher' : manager
}, true));
}
return warnings;
},
requires() {
const requires = [];
const required = (this.version?.annotations?.[CATALOG_ANNOTATIONS.REQUIRES_GVK] || '').split(/\s*,\s*/).filter((x) => !!x).reverse();
if ( required.length ) {
for ( const gvr of required ) {
if ( this.$store.getters['catalog/isInstalled']({ gvr }) ) {
continue;
}
const provider = this.provider(gvr);
if ( provider ) {
const url = this.$router.resolve(this.chartLocation(true, provider)).href;
requires.push(this.t('catalog.install.error.requiresFound', {
url,
name: provider.name
}, true));
} else {
requires.push(this.t('catalog.install.error.requiresMissing', { name: gvr }));
}
}
}
return requires;
},
currentVersion() {
return this.existing?.spec?.chart?.metadata?.version;
},
targetVersion() {
return this.version ? this.version.version : this.query.versionName;
},
action() {
if (!this.existing) {
return {
name: 'install', tKey: 'install', icon: 'icon-plus'
};
}
if (this.currentVersion === this.targetVersion) {
return {
name: 'editVersion', tKey: 'edit', icon: 'icon-edit'
};
}
if (compare(this.currentVersion, this.targetVersion) < 0) {
return {
name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
};
}
return {
name: 'downgrade', tKey: 'downgrade', icon: 'icon-downgrade-alt'
};
},
isChartTargeted() {
return this.chart?.targetNamespace && this.chart?.targetName;
},
hasQuestions() {
return this.versionInfo && !!this.versionInfo.questions;
},
},
methods: {
/**
* Populate `this.chart`
*
* `chart` used to be a computed property pointing at getter catalog/chart
*
* this however stopped recalculating given changes to the store
*
* (the store would populate a charts collection, which the getter uses to find the chart,
* however this did not kick off the computed property, so this.charts was not populated)
*
* Now we find and cache the chart
*/
fetchStoreChart() {
if (!this.chart && this.repo && this.query.chartName) {
this.chart = this.$store.getters['catalog/chart']({
repoType: this.query.repoType,
repoName: this.query.repoName,
chartName: this.query.chartName,
includeHidden: true,
showDeprecated: this.showDeprecated
});
}
return this.chart;
},
async fetchChart() {
this.versionInfoError = null;
await Promise.all([
this.$store.dispatch('catalog/load'),
this.$store.dispatch('cluster/findAll', { type: CATALOG.APP })
]);
this.fetchStoreChart();
if ( this.query.appNamespace && this.query.appName ) {
// First check the URL query for an app name and namespace.
// Use those values to check for a catalog app resource.
// If found, set the form to edit mode. If not, set the
// form to create mode.
try {
this.existing = await this.$store.dispatch('cluster/find', {
type: CATALOG.APP,
id: `${ this.query.appNamespace }/${ this.query.appName }`,
});
await this.existing?.fetchValues(true);
this.mode = _EDIT;
} catch (e) {
this.mode = _CREATE;
this.existing = null;
}
} else if ( this.chart?.targetNamespace && this.chart?.targetName ) {
// If the app name and namespace values are not provided in the
// query, fall back on target values defined in the Helm chart itself.
// Ask to install a special chart with fixed namespace/name
// or edit it if there's an existing install.
try {
this.existing = await this.$store.dispatch('cluster/find', {
type: CATALOG.APP,
id: `${ this.chart.targetNamespace }/${ this.chart.targetName }`,
});
this.mode = _EDIT;
} catch (e) {
this.mode = _CREATE;
this.existing = null;
}
} else if (this.chart) {
const matching = this.chart.matchingInstalledApps;
if (matching.length === 1) {
this.existing = matching[0];
this.mode = _EDIT;
} else {
this.mode = _CREATE;
}
} else {
// Regular create
this.mode = _CREATE;
}
if ( !this.chart ) {
return;
}
// If no version is given in the URL query,
// use the first version provided by the Helm chart
// as the default.
if ( !this.query.versionName && this.chart.versions?.length ) {
if (this.showPreRelease) {
this.query.versionName = this.chart.versions[0].version;
} else {
const firstRelease = this.chart.versions.find((v) => !isPrerelease(v.version));
this.query.versionName = firstRelease?.version || this.chart.versions[0].version;
}
}
if ( !this.query.versionName ) {
return;
}
try {
this.version = this.$store.getters['catalog/version']({
repoType: this.query.repoType,
repoName: this.query.repoName,
chartName: this.query.chartName,
versionName: this.query.versionName,
showDeprecated: this.showDeprecated
});
} catch (e) {
console.error('Unable to fetch Version: ', e); // eslint-disable-line no-console
}
if (!this.version) {
console.warn('No version found: ', this.query.repoType, this.query.repoName, this.query.chartName, this.query.versionName);// eslint-disable-line no-console
}
try {
this.versionInfo = await this.$store.dispatch('catalog/getVersionInfo', {
repoType: this.query.repoType,
repoName: this.query.repoName,
chartName: this.query.chartName,
versionName: this.query.versionName
});
// Here we set us versionInfo. The returned
// object contains everything all info
// about a currently installed app, and it has the
// following keys:
//
// - appReadme: A short overview of what the app does. This
// forms the first few paragraphs of the chart info when
// you install a Helm chart app through Rancher.
// - chart: Metadata about the Helm chart, including the
// name and version.
// - readme: This is more detailed information that appears
// under the heading "Chart Information (Helm README)" when
// you install or upgrade a Helm chart app through Rancher,
// below the app README.
// - values: All Helm chart values for the currently installed
// app.
} catch (e) {
this.versionInfoError = e;
console.error('Unable to fetch VersionInfo: ', e); // eslint-disable-line no-console
}
}, // End of fetchChart
// Charts have an annotation that specifies any additional charts that should be installed at the same time eg CRDs
async fetchAutoInstallInfo() {
const out = [];
/*
An example value for auto is ["rancher-monitoring-crd=match"].
It is an array of chart names that lets Rancher know of other
charts that should be auto-installed at the same time.
*/
const auto = (this.version?.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL] || '').split(/\s*,\s*/).filter((x) => !!x).reverse();
for ( const constraint of auto ) {
const provider = this.$store.getters['catalog/versionSatisfying']({
constraint,
repoName: this.chart.repoName,
repoType: this.chart.repoType,
chartVersion: this.version.version,
});
/* An example return value for "provider":
[
{
"name": "rancher-monitoring-crd",
"version": "100.1.3+up19.0.3",
"description": "Installs the CRDs for rancher-monitoring.",
"apiVersion": "v1",
"annotations": {
"catalog.cattle.io/certified": "rancher",
"catalog.cattle.io/hidden": "true",
"catalog.cattle.io/namespace": "cattle-monitoring-system",
"catalog.cattle.io/release-name": "rancher-monitoring-crd"
},
"type": "application",
"urls": [
"https://192.168.0.18:8005/k8s/clusters/c-m-hhpg69fv/v1/catalog.cattle.io.clusterrepos/rancher-charts?chartName=rancher-monitoring-crd&link=chart&version=100.1.3%2Bup19.0.3"
],
"created": "2022-04-27T10:04:18.343124-07:00",
"digest": "ecf07ba23a9cdaa7ffbbb14345d94ea1240b7f3b8e0ce9be4640e3e585c484e2",
"key": "cluster/rancher-charts/rancher-monitoring-crd/100.1.3+up19.0.3",
"repoType": "cluster",
"repoName": "rancher-charts"
}
]
*/
if ( provider ) {
try {
const crdVersionInfo = await this.$store.dispatch('catalog/getVersionInfo', {
repoType: provider.repoType,
repoName: provider.repoName,
chartName: provider.name,
versionName: provider.version
});
let existingCRDApp;
// search for an existing crd app to track any non-default values used on the previous install/upgrade
if (this.mode === _EDIT) {
const targetNamespace = crdVersionInfo?.chart?.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE];
const targetName = crdVersionInfo?.chart?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
if (targetName && targetNamespace) {
existingCRDApp = await this.$store.dispatch('cluster/find', {
type: CATALOG.APP,
id: `${ targetNamespace }/${ targetName }`,
});
}
}
if (existingCRDApp) {
await existingCRDApp.fetchValues(true);
// spec.values are any non-default values the user configured
// the installation form should show these, as well as any default values from the chart
const existingValues = clone(existingCRDApp.values || {});
const defaultValues = clone(existingCRDApp.chartValues || {});
crdVersionInfo.existingValues = existingValues;
crdVersionInfo.allValues = merge(defaultValues, existingValues);
} else {
// allValues will potentially be updated in the UI - we want to track this separately from values to avoid mutating the chart object in the store
// this is similar to userValues for the main chart
crdVersionInfo.allValues = clone(crdVersionInfo.values);
}
out.push(crdVersionInfo);
} catch (e) {
console.error('Unable to fetch VersionInfo: ', e); // eslint-disable-line no-console
}
} else {
this.errors.push(`This chart requires ${ constraint } but no matching chart was found`);
}
}
this['autoInstallInfo'] = out;
},
selectVersion({ id: version }) {
this.$router.applyQuery({ [VERSION]: version });
},
provider(gvr) {
return this.$store.getters['catalog/versionProviding']({
gvr,
repoName: this.chart.repoName,
repoType: this.chart.repoType
});
},
/**
* Location of chart install or details page for either the current chart or from gvr
*/
chartLocation(install = false, prov) {
const provider = prov || {
repoType: this.chart.repoType,
repoName: this.chart.repoName,
name: this.chart.chartName,
version: this.query.versionName,
};
const query = {
[REPO_TYPE]: provider.repoType,
[REPO]: provider.repoName,
[CHART]: provider.name,
[VERSION]: provider.version,
};
if (this.showDeprecated) {
query[DEPRECATED_QUERY] = this.query.deprecated;
}
return {
name: install ? 'c-cluster-apps-charts-install' : 'c-cluster-apps-charts-chart',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
},
query
};
},
appLocation() {
return this.existing?.detailLocation || {
name: `c-cluster-product-resource`,
params: {
product: this.$store.getters['productId'],
cluster: this.$store.getters['clusterId'],
resource: CATALOG.APP,
}
};
},
clusterToolsLocation() {
const query = {};
if (this.showDeprecated) {
query[DEPRECATED_QUERY] = this.query.deprecated;
}
return {
name: `c-cluster-explorer-tools`,
params: {
product: EXPLORER,
cluster: this.$store.getters['clusterId'],
resource: CATALOG.APP,
},
query
};
},
clustersLocation() {
return {
name: 'c-cluster-product-resource',
params: {
cluster: this.$route.params.cluster,
product: MANAGER,
resource: CAPI.RANCHER_CLUSTER,
},
};
}
},
};