diff --git a/pkg/harvester/components/SettingList.vue b/pkg/harvester/components/SettingList.vue index ffb3c2cb..ea9566fa 100644 --- a/pkg/harvester/components/SettingList.vue +++ b/pkg/harvester/components/SettingList.vue @@ -3,7 +3,9 @@ import { mapGetters } from 'vuex'; import ActionMenu from '@shell/components/ActionMenuShell'; import { Banner } from '@components/Banner'; import AsyncButton from '@shell/components/AsyncButton'; -import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings'; +import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; +import { DOC } from '../config/doc-links'; +import { docLink } from '../utils/feature-flags'; const CATEGORY = { ui: [ @@ -96,7 +98,7 @@ export default { return true; } - const description = this.t(setting.description, {}, true)?.toLowerCase() || ''; + const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || ''; // filter by description if (description.includes(searchQuery)) { @@ -193,6 +195,19 @@ export default { } buttonDone(false); } + }, + + getDocLinkParams(setting) { + const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id]; + + if (settingConfig?.docPath) { + const version = this.$store.getters['harvester-common/getServerVersion'](); + const url = docLink(DOC[settingConfig.docPath], version); + + return { url }; + } + + return {}; } }, }; @@ -223,7 +238,7 @@ export default { Experimental -

+

+import CreateEditView from '@shell/mixins/create-edit-view'; +import { LabeledInput } from '@components/Form/LabeledInput'; +import LabeledSelect from '@shell/components/form/LabeledSelect'; +import { RadioGroup } from '@components/Form/Radio'; +import { mapGetters } from 'vuex'; + +export default { + name: 'HarvesterUpgradeConfig', + + components: { + LabeledInput, + LabeledSelect, + RadioGroup + }, + mixins: [CreateEditView], + + data() { + let parseDefaultValue = {}; + + try { + parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default); + } catch (error) { + parseDefaultValue = JSON.parse(this.value.default); + } + parseDefaultValue = this.normalizeValue(parseDefaultValue); + + return { + parseDefaultValue, + errors: [] + }; + }, + + computed: { + ...mapGetters({ t: 'i18n/t' }), + strategyOptions() { + return [ + { value: 'sequential', label: 'sequential' }, + { value: 'skip', label: 'skip' }, + { value: 'parallel', label: 'parallel' } + ]; + } + }, + + created() { + this.update(); + }, + + methods: { + normalizeValue(obj) { + if (!obj.imagePreloadOption) { + obj.imagePreloadOption = { strategy: { type: 'sequential' } }; + } + if (!obj.imagePreloadOption.strategy) { + obj.imagePreloadOption.strategy = { type: 'sequential' }; + } + if (!obj.imagePreloadOption.strategy.type) { + obj.imagePreloadOption.strategy.type = 'sequential'; + } + // Only set concurrency if type is 'parallel' + if (obj.imagePreloadOption.strategy.type === 'parallel') { + if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') { + obj.imagePreloadOption.strategy.concurrency = 0; + } + } else { + delete obj.imagePreloadOption.strategy.concurrency; + } + if (typeof obj.restoreVM !== 'boolean') { + obj.restoreVM = false; + } + + return obj; + }, + update() { + try { + // Clone to avoid mutating the form state + const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue)); + + if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) { + if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') { + delete valueToSave.imagePreloadOption.strategy.concurrency; + } + } + + this.value['value'] = JSON.stringify(valueToSave, null, 2); + this.errors = []; + } catch (e) { + this.errors = ['Invalid JSON']; + } + } + }, + + watch: { + value: { + handler(neu) { + let parseDefaultValue; + + try { + parseDefaultValue = JSON.parse(neu.value); + } catch (err) { + parseDefaultValue = JSON.parse(this.value.default); + } + parseDefaultValue = this.normalizeValue(parseDefaultValue); + this['parseDefaultValue'] = parseDefaultValue; + this.update(); + }, + deep: true + } + } +}; + + + + + diff --git a/pkg/harvester/config/doc-links.js b/pkg/harvester/config/doc-links.js index dae259d5..754f06b3 100644 --- a/pkg/harvester/config/doc-links.js +++ b/pkg/harvester/config/doc-links.js @@ -4,6 +4,7 @@ export const DOC = { RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`, KSMTUNED_MODE: `/host/#ksmtuned-mode`, UPGRADE_URL: `/upgrade/index`, + UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`, STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`, SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`, }; diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js index 02fe176d..6899ac70 100644 --- a/pkg/harvester/config/feature-flags.js +++ b/pkg/harvester/config/feature-flags.js @@ -21,6 +21,7 @@ const FEATURE_FLAGS = { 'vmSnapshotQuota', 'longhornV2LVMSupport', 'improveMaintenanceMode', + 'upgradeConfigSetting' ], 'v1.4.1': [], 'v1.4.2': [ diff --git a/pkg/harvester/config/settings.ts b/pkg/harvester/config/settings.ts index 792e72c8..160389e6 100644 --- a/pkg/harvester/config/settings.ts +++ b/pkg/harvester/config/settings.ts @@ -34,6 +34,7 @@ export const HCI_SETTING = { KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes', LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled', ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio', + UPGRADE_CONFIG: 'upgrade-config', }; export const HCI_ALLOWED_SETTINGS = { @@ -96,6 +97,12 @@ export const HCI_ALLOWED_SETTINGS = { featureFlag: 'longhornV2LVMSupport' }, [HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' }, + [HCI_SETTING.UPGRADE_CONFIG]: { + kind: 'json', + from: 'import', + featureFlag: 'upgradeConfigSetting', + docPath: 'UPGRADE_CONFIG_URL' + }, }; export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { diff --git a/pkg/harvester/edit/harvesterhci.io.setting.vue b/pkg/harvester/edit/harvesterhci.io.setting.vue index 9257c407..4b53d5c8 100644 --- a/pkg/harvester/edit/harvesterhci.io.setting.vue +++ b/pkg/harvester/edit/harvesterhci.io.setting.vue @@ -6,6 +6,8 @@ import LabeledSelect from '@shell/components/form/LabeledSelect'; import { TextAreaAutoGrow } from '@components/Form/TextArea'; import UnitInput from '@shell/components/form/UnitInput'; import CreateEditView from '@shell/mixins/create-edit-view'; +import { DOC } from '../config/doc-links'; +import { docLink } from '../utils/feature-flags'; import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; @@ -58,7 +60,13 @@ export default { return { setting, - description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`), + description: isHarvester ? t( + `advancedSettings.descriptions.harv-${ this.value.id }`, + this.getDocLinkParams() + ) : t( + `advancedSettings.descriptions.${ this.value.id }`, + this.getDocLinkParams() + ), editHelp: t(`advancedSettings.editHelp.${ this.value.id }`), enumOptions, canReset, @@ -171,6 +179,18 @@ export default { if (typeof this.$refs.settingComp?.useDefault === 'function') { this.$refs.settingComp.useDefault(); } + }, + getDocLinkParams() { + const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id]; + + if (setting?.docPath) { + const version = this.$store.getters['harvester-common/getServerVersion'](); + const url = docLink(DOC[setting.docPath], version); + + return { url }; + } + + return {}; } } }; diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 00bb201f..ba56a61b 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -694,7 +694,7 @@ harvester: network: label: Network Data Template title: "Network Data:" - tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. Learn more" + tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. Learn more" scheduling: affinity: anyNode: 'Run virtual machine on any available node' @@ -1128,6 +1128,10 @@ harvester: uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process." deleteImage: Please select an image to delete. deleteSuccess: "{name} deleted successfully." + imagePreloadStrategy: Image Preload Strategy + restoreVM: Restore VM + strategyType: Strategy Type + concurrency: Concurrency harvesterMonitoring: label: Harvester Monitoring section: @@ -1621,6 +1625,7 @@ advancedSettings: 'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.' 'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. ' 'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.' + 'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in settings/upgrade-config' typeLabel: kubevirt.io.virtualmachine: |- diff --git a/pkg/harvester/list/harvesterhci.io.setting.vue b/pkg/harvester/list/harvesterhci.io.setting.vue index 05edc170..4452ebcd 100644 --- a/pkg/harvester/list/harvesterhci.io.setting.vue +++ b/pkg/harvester/list/harvesterhci.io.setting.vue @@ -116,7 +116,15 @@ export default { return { ...s, description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`, - customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized + customized: (!s.readOnly && s.data.value && ( + s.kind === 'json' ? (() => { + try { + return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default)); + } catch { + return s.data.value !== s.data.default; + } + })() : s.data.value !== s.data.default + )) || s.data.hasCustomized }; }); }