dashboard/shell/utils/cluster.js

492 lines
16 KiB
JavaScript

import semver from 'semver';
import { camelToTitle } from '@shell/utils/string';
import { CAPI } from '@shell/config/labels-annotations';
import { MANAGEMENT, VIRTUAL_HARVESTER_PROVIDER } from '@shell/config/types';
import { SETTING } from '@shell/config/settings';
import { PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
import { compare, sortable } from '@shell/utils/version';
import { sortBy } from '@shell/utils/sort';
import { HARVESTER_CONTAINER, SCHEDULING_CUSTOMIZATION } from '@shell/store/features';
import { _CREATE, _EDIT } from '@shell/config/query-params';
import isEmptyLodash from 'lodash/isEmpty';
import { set, diff, isEmpty, clone } from '@shell/utils/object';
/**
* Combination of paginationFilterHiddenLocalCluster and paginationFilterOnlyKubernetesClusters
*
* @param {*} store
* @returns PaginationParam[]
*/
export function paginationFilterClusters(store, filterMgmtCluster = true) {
const paginationRequestFilters = [];
const pFilterOnlyKubernetesClusters = paginationFilterOnlyKubernetesClusters(store);
const pFilterHiddenLocalCluster = paginationFilterHiddenLocalCluster(store, filterMgmtCluster);
if (pFilterOnlyKubernetesClusters) {
paginationRequestFilters.push(...pFilterOnlyKubernetesClusters);
}
if (pFilterHiddenLocalCluster) {
paginationRequestFilters.push(pFilterHiddenLocalCluster);
}
return paginationRequestFilters;
}
/**
* The vai backed api's `filter` equivalent of `filterHiddenLocalCluster`
*
* @export
* @param {*} store
* @returns PaginationParam | null
*/
export function paginationFilterHiddenLocalCluster(store, filterMgmtCluster = true) {
const hideLocalSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.HIDE_LOCAL_CLUSTER) || {};
const value = hideLocalSetting.value || hideLocalSetting.default || 'false';
const hideLocal = value === 'true';
if (!hideLocal) {
return null;
}
const filter = filterMgmtCluster ? [
new PaginationFilterField({
field: `spec.internal`,
value: false,
})
] : [
new PaginationFilterField({
field: `id`,
value: 'fleet-local/local',
exact: true,
equals: false,
})
];
return PaginationParamFilter.createMultipleFields(filter);
}
/**
* The vai backed api's `filter` equivalent of `filterOnlyKubernetesClusters`
*
* @export
* @param {*} store
* @returns PaginationParam | null
*/
export function paginationFilterOnlyKubernetesClusters(store) {
const openHarvesterContainerWorkload = store.getters['features/get'](HARVESTER_CONTAINER);
if (openHarvesterContainerWorkload) {
// Show harvester clusters
return null;
}
// Filter out harvester clusters
return [
PaginationParamFilter.createSingleField(new PaginationFilterField({
field: `metadata.labels[${ CAPI.PROVIDER }]`,
equals: false,
value: VIRTUAL_HARVESTER_PROVIDER,
exact: true
})),
PaginationParamFilter.createSingleField(new PaginationFilterField({
field: `status.provider`,
equals: false,
value: VIRTUAL_HARVESTER_PROVIDER,
exact: true
}))
];
}
/**
* Filter out any clusters that are not Kubernetes Clusters
**/
export function filterOnlyKubernetesClusters(mgmtClusters, store) {
const openHarvesterContainerWorkload = store.getters['features/get'](HARVESTER_CONTAINER);
if (openHarvesterContainerWorkload) {
// Show harvester clusters
return mgmtClusters;
}
// Filter out harvester clusters
return mgmtClusters?.filter((c) => !isHarvesterCluster(c));
}
export function isHarvesterCluster(mgmtCluster) {
// Use the provider if it is set otherwise use the label
const provider = mgmtCluster?.metadata?.labels?.[CAPI.PROVIDER] || mgmtCluster?.status?.provider;
return provider === VIRTUAL_HARVESTER_PROVIDER;
}
export function isHarvesterSatisfiesVersion(version = '') {
if (version.startsWith('v1.21.4+rke2r')) {
const rkeVersion = version.replace(/.+rke2r/i, '');
return Number(rkeVersion) >= 4;
} else {
return semver.satisfies(semver.coerce(version), '>=v1.21.4+rke2r4');
}
}
export function filterHiddenLocalCluster(mgmtClusters, store) {
const hideLocalSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.HIDE_LOCAL_CLUSTER) || {};
const value = hideLocalSetting.value || hideLocalSetting.default || 'false';
const hideLocal = value === 'true';
if (!hideLocal) {
return mgmtClusters;
}
return mgmtClusters.filter((c) => {
const target = c.mgmt || c;
return !target.isLocal;
});
}
const clusterNameSegments = /([A-Za-z]+|\d+)/g;
/**
* Shortens an input string based on the number of segments it contains.
* @param {string} input - The input string to be shortened.
* @returns {string} - The shortened string.
* @example smallIdentifier('local') => 'lcl'
* @example smallIdentifier('word-wide-web') => 'www'
*/
export function abbreviateClusterName(input) {
if (!input) {
return '';
}
if (input.length <= 3) {
return input;
}
const segments = input.match(clusterNameSegments);
if (!segments) return ''; // In case no valid segments are found
let result = '';
switch (segments.length) {
case 1: {
const word = segments[0];
result = `${ word[0] }${ word[Math.floor(word.length / 2)] }${ word[word.length - 1] }`;
break;
}
case 2: {
const w1 = `${ segments[0][0] }`;
const w2 = `${ segments[0].length >= 2 ? segments[0][segments[0].length - 1] : segments[1][0] }`;
const w3 = `${ segments[1][segments[1].length - 1] }`;
result = w1 + w2 + w3;
break;
}
default:
result = segments.slice(0, 2).map((segment) => segment[0]).join('') + segments.slice(-1)[0].slice(-1);
}
return result;
}
export function labelForAddon(store, name, configuration = true) {
const addon = camelToTitle(name.replace(/^(rke|rke2|rancher)-/, ''));
const fallback = `${ configuration ? '' : 'Add-on: ' }${ addon }`;
const key = `cluster.addonChart."${ name }"${ configuration ? '.configuration' : '.label' }`;
return store.getters['i18n/withFallback'](key, null, fallback);
}
function getMostRecentPatchVersions(sortedVersions) {
// Get the most recent patch version for each Kubernetes minor version.
const versionMap = {};
sortedVersions.forEach((version) => {
const majorMinor = `${ semver.major(version.value) }.${ semver.minor(version.value) }`;
if (!versionMap[majorMinor]) {
// Because we start with a sorted list of versions, we know the
// highest patch version is first in the list, so we only keep the
// first of each minor version in the list.
versionMap[majorMinor] = version.value;
}
});
return versionMap;
}
export function filterOutDeprecatedPatchVersions(allVersions, currentVersion) {
// Get the most recent patch version for each Kubernetes minor version.
const mostRecentPatchVersions = getMostRecentPatchVersions(allVersions);
const filteredVersions = allVersions.filter((version) => {
// Always show pre-releases
if (semver.prerelease(version.value)) {
return true;
}
const majorMinor = `${ semver.major(version.value) }.${ semver.minor(version.value) }`;
// Always show current version, else show if we haven't shown anything for this major.minor version yet
if (version.value === currentVersion || mostRecentPatchVersions[majorMinor] === version.value) {
return true;
}
return false;
});
return filteredVersions;
}
export function getAllOptionsAfterCurrentVersion(store, versions, currentVersion, defaultVersion) {
const out = (versions || []).filter((obj) => !!obj.serverArgs).map((obj) => {
let disabled = false;
let experimental = false;
let isCurrentVersion = false;
let label = obj.id;
if (currentVersion) {
disabled = compare(obj.id, currentVersion) < 0;
isCurrentVersion = compare(obj.id, currentVersion) === 0;
}
if (defaultVersion) {
experimental = compare(defaultVersion, obj.id) < 0;
}
if (isCurrentVersion) {
label = `${ label } ${ store.getters['i18n/t']('cluster.kubernetesVersion.current') }`;
}
if (experimental) {
label = `${ label } ${ store.getters['i18n/t']('cluster.kubernetesVersion.experimental') }`;
}
return {
label,
value: obj.id,
sort: sortable(obj.id),
serverArgs: obj.serverArgs,
agentArgs: obj.agentArgs,
charts: obj.charts,
disabled,
};
});
if (currentVersion && !out.find((obj) => obj.value === currentVersion)) {
out.push({
label: `${ currentVersion } ${ store.getters['i18n/t']('cluster.kubernetesVersion.current') }`,
value: currentVersion,
sort: sortable(currentVersion),
});
}
const sorted = sortBy(out, 'sort:desc');
const mostRecentPatchVersions = getMostRecentPatchVersions(sorted);
const sortedWithDeprecatedLabel = sorted.map((optionData) => {
const majorMinor = `${ semver.major(optionData.value) }.${ semver.minor(optionData.value) }`;
if (mostRecentPatchVersions[majorMinor] === optionData.value) {
return optionData;
}
return {
...optionData,
label: `${ optionData.label } ${ store.getters['i18n/t']('cluster.kubernetesVersion.deprecated') }`
};
});
return sortedWithDeprecatedLabel;
}
export async function initSchedulingCustomization(value, features, store, mode) {
const schedulingCustomizationFeatureEnabled = features(SCHEDULING_CUSTOMIZATION);
let clusterAgentDefaultPC = null;
let clusterAgentDefaultPDB = null;
let schedulingCustomizationOriginallyEnabled = false;
const errors = [];
try {
clusterAgentDefaultPC = JSON.parse((await store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS })).value) || null;
} catch (e) {
errors.push(e);
}
try {
clusterAgentDefaultPDB = JSON.parse((await store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET })).value) || null;
} catch (e) {
errors.push(e);
}
if (schedulingCustomizationFeatureEnabled && mode === _CREATE && isEmptyLodash(value?.clusterAgentDeploymentCustomization?.schedulingCustomization)) {
set(value, 'clusterAgentDeploymentCustomization.schedulingCustomization', { priorityClass: clusterAgentDefaultPC, podDisruptionBudget: clusterAgentDefaultPDB });
}
if (mode === _EDIT && !!value?.clusterAgentDeploymentCustomization?.schedulingCustomization) {
schedulingCustomizationOriginallyEnabled = true;
}
return {
clusterAgentDefaultPC, clusterAgentDefaultPDB, schedulingCustomizationFeatureEnabled, schedulingCustomizationOriginallyEnabled, errors
};
}
/**
* Recursively filters a `diffs` object to only include differences that are relevant to the user.
* A difference is considered relevant if the user has provided a custom value for that specific field.
*
* @param {object} diffs - The object representing the differences between two chart versions' default values.
* @param {object} userVals - The object containing the user's custom values.
* @returns {object} A new object containing only the relevant differences.
*/
export function _addonConfigPreserveFilter(diffs, userVals) {
const filtered = {};
for (const key of Object.keys(diffs)) {
const diffVal = diffs[key];
const userVal = userVals?.[key];
const isDiffObject = typeof diffVal === 'object' && diffVal !== null && !Array.isArray(diffVal);
const isUserObject = typeof userVal === 'object' && userVal !== null && !Array.isArray(userVal);
// If both the diff and user value are objects, we need to recurse into them.
if (isDiffObject && isUserObject) {
const nestedFiltered = _addonConfigPreserveFilter(diffVal, userVal);
if (!isEmpty(nestedFiltered)) {
filtered[key] = nestedFiltered;
}
} else if (userVal !== undefined) {
// If the user has provided a value for this key, the difference is relevant.
filtered[key] = diffVal;
}
}
return filtered;
}
/**
* Processes a single add-on version change. It fetches the old and new chart information,
* calculates the differences in default values, and filters them based on user's customizations.
* If there are no significant differences, it preserves the user's custom values for the new version.
*
* @param {object} store The Vuex store.
* @param {object} userChartValues The user's customized chart values.
* @param {string} chartName The name of the chart to process.
* @param {object} oldAddon The addon information from the previous Kubernetes version.
* @param {object} newAddon The addon information from the new Kubernetes version.
* @returns {object|null} An object containing the diff and a preserve flag, or null on error.
*/
async function _addonConfigPreserveProcess(store, userChartValues, chartName, oldAddon, newAddon) {
if (chartName.includes('none')) {
return null;
}
try {
const [oldVersionInfo, newVersionInfo] = await Promise.all([
store.dispatch('catalog/getVersionInfo', {
repoType: 'cluster',
repoName: oldAddon.repo,
chartName,
versionName: oldAddon.version,
}),
store.dispatch('catalog/getVersionInfo', {
repoType: 'cluster',
repoName: newAddon.repo,
chartName,
versionName: newAddon.version,
})
]);
const oldDefaults = oldVersionInfo.values;
const newDefaults = newVersionInfo.values;
const defaultsDifferences = diff(oldDefaults, newDefaults);
const userOldValues = userChartValues[`${ chartName }-${ oldAddon.version }`];
// We only care about differences in values that the user has actually customized.
// If the user hasn't touched a value, a change in its default should not be considered a breaking change.
const defaultsAndUserDifferences = userOldValues ? _addonConfigPreserveFilter(defaultsDifferences, userOldValues) : {};
return {
diff: defaultsAndUserDifferences,
preserve: isEmpty(defaultsAndUserDifferences)
};
} catch (e) {
console.error(`Failed to get chart version info for diff for chart ${ chartName }`, e); // eslint-disable-line no-console
return null;
}
}
/**
* @typedef {object} AddonPreserveContext
* @property {object} addonConfigDiffs - An object that stores the diffs.
* @property {string[]} addonNames - An array of addon names to check.
* @property {object} $store - The Vuex store.
* @property {object} userChartValues - The user's customized chart values.
*
* When the Kubernetes version is changed, this method is called to handle the add-on configurations
* for all enabled addons. It checks if an addon's version has changed and, if so, determines if the
* user's custom configurations should be preserved for the new version.
*
* The goal is to avoid showing a confirmation dialog for changes in default values that the user has not customized.
*
* @param {AddonPreserveContext} context The context object from the component.
* @param {object} oldCharts The charts object from the K8s release object being changed from.
* @param {object} newCharts The charts object from the K8s release object being changed to.
*/
export async function addonConfigPreserve(context, oldCharts, newCharts) {
const {
addonConfigDiffs,
addonNames,
$store,
userChartValues
} = context;
if (!oldCharts || !newCharts) {
return;
}
// Clear the diffs object for the new run
for (const key in addonConfigDiffs) {
delete addonConfigDiffs[key];
}
// Iterate through the addons that are enabled for the cluster.
for (const chartName of addonNames) {
const oldAddon = oldCharts[chartName];
const newAddon = newCharts[chartName];
// If the addon didn't exist in the old K8s version, there's nothing to compare.
if (!oldAddon) {
continue;
}
// Check if the add-on version has changed.
if (newAddon && newAddon.version !== oldAddon.version) {
const result = await _addonConfigPreserveProcess($store, userChartValues, chartName, oldAddon, newAddon);
if (result) {
addonConfigDiffs[chartName] = result.diff;
if (result.preserve) {
const oldKey = `${ chartName }-${ oldAddon.version }`;
const newKey = `${ chartName }-${ newAddon.version }`;
// If custom values exist for the old version, and none exist for the new version,
// copy the values to the new key to preserve them.
if (userChartValues[oldKey] && !userChartValues[newKey]) {
userChartValues[newKey] = clone(userChartValues[oldKey]);
}
}
}
}
}
}