feat: add resume button for upgrade paused node (#698)

* feat: add nodeUpgradeOption setting

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add resume button when node paused

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add feature flag in v1.7.0

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-01-30 15:12:00 +08:00 committed by GitHub
parent 473c1ba355
commit 77599900b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 13 deletions

View File

@ -1,6 +1,8 @@
<script> <script>
import Collapse from '@shell/components/Collapse'; import Collapse from '@shell/components/Collapse';
import PercentageBar from '@shell/components/PercentageBar'; import PercentageBar from '@shell/components/PercentageBar';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export default { export default {
name: 'HarvesterUpgradeProgressList', name: 'HarvesterUpgradeProgressList',
@ -25,13 +27,45 @@ export default {
} }
}, },
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
},
data() { data() {
return { open: true }; return { open: true };
}, },
computed: {
showResumeButton() {
return this.title === 'Upgrading Node';
},
latestUpgradeCR() {
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
},
methods: { methods: {
handleSwitch() { handleSwitch() {
this.open = !this.open; this.open = !this.open;
},
async resumeNodeUpgrade(nodeName) {
if (!this.latestUpgradeCR || !nodeName) return;
try {
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
const upgradePauseMap = JSON.parse(upgradePauseMapString);
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
upgradePauseMap[`${ nodeName }`] = 'unpause';
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
await this.latestUpgradeCR.save();
} catch (e) {
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
return false;
}
} }
} }
}; };
@ -63,12 +97,28 @@ export default {
v-for="(item, i) in list" v-for="(item, i) in list"
:key="i" :key="i"
> >
<p> <div class="upgrade-node-header">
{{ item.name }} <span <div class="upgrade-node-title">
class="status" <p>
:class="{ [item.state]: true }" {{ item.name }}
>{{ item.state }}</span> </p>
</p> <span
class="status"
:class="{ [item.state]: true }"
>
{{ item.state }}
</span>
</div>
<button
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
type="button"
class="btn bg-info btn-sm"
data-testid="add-item"
@click="resumeNodeUpgrade(item.name)"
>
{{ t('action.resume') }}
</button>
</div>
<PercentageBar <PercentageBar
:model-value="item.percent" :model-value="item.percent"
preferred-direction="MORE" preferred-direction="MORE"
@ -102,10 +152,21 @@ export default {
} }
} }
.custom-content { .custom-content {
margin-bottom: 14px; .upgrade-node-title {
p { flex: 1 0 80%;
margin-right: 10px;
display: flex;
justify-content: space-between;
}
.upgrade-node-header {
display:flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px; margin-bottom: 4px;
} }
margin-bottom: 14px;
.status { .status {
float: right; float: right;
} }
@ -117,6 +178,8 @@ export default {
} }
.warning { .warning {
color: var(--error); color: var(--error);
margin-bottom: 8px;
margin-top: 4px;
} }
} }
} }

View File

@ -4,6 +4,8 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
export default { export default {
name: 'HarvesterUpgradeConfig', name: 'HarvesterUpgradeConfig',
@ -15,6 +17,13 @@ export default {
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
await allHash(hash);
},
data() { data() {
let parseDefaultValue = {}; let parseDefaultValue = {};
@ -39,7 +48,25 @@ export default {
{ value: 'skip', label: 'skip' }, { value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' } { value: 'parallel', label: 'parallel' }
]; ];
} },
nodeUpgradeOptions() {
return [
{ value: 'auto', label: 'auto' },
{ value: 'manual', label: 'manual' }
];
},
nodesOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.map((node) => ({ value: node.id, label: node.name }));
},
showPauseNodes() {
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
}, },
created() { created() {
@ -48,6 +75,18 @@ export default {
methods: { methods: {
normalizeValue(obj) { normalizeValue(obj) {
// handle nodeUpgradeOption.strategy
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
delete obj.nodeUpgradeOption.strategy.pauseNodes;
}
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
}
}
// handle imagePreloadOption.strategy
if (!obj.imagePreloadOption) { if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } }; obj.imagePreloadOption = { strategy: { type: 'sequential' } };
} }
@ -105,8 +144,8 @@ export default {
this.update(); this.update();
}, },
deep: true deep: true
} },
} },
}; };
</script> </script>
@ -144,6 +183,28 @@ export default {
:labels="[t('generic.enabled'), t('generic.disabled')]" :labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update" @update:value="update"
/> />
<div v-if="resumeUpgradePausedNodeEnabled">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
class="mb-20 label-select"
:mode="mode"
:label="t('harvester.setting.upgrade.strategy')"
:options="nodeUpgradeOptions"
@update:value="update"
/>
<LabeledSelect
v-if="showPauseNodes"
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
class="mb-20 label-select"
:clearable="true"
:multiple="true"
:mode="mode"
:label="t('harvester.setting.upgrade.pauseNodes')"
:options="nodesOptions"
@update:value="update"
/>
</div>
<div <div
v-if="errors.length" v-if="errors.length"
class="error" class="error"

View File

@ -54,8 +54,11 @@ const FEATURE_FLAGS = {
'lhV2VolExpansion', 'lhV2VolExpansion',
'l2VlanTrunkMode', 'l2VlanTrunkMode',
'kubevirtMigration', 'kubevirtMigration',
'hotplugNic' 'hotplugNic',
] 'resumeUpgradePausedNode',
],
'v1.7.1': [],
'v1.8.0': []
}; };
const generateFeatureFlags = () => { const generateFeatureFlags = () => {

View File

@ -78,4 +78,5 @@ export const HCI = {
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes', VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass', VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address', MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
}; };

View File

@ -1295,7 +1295,10 @@ harvester:
deleteImage: Please select an image to delete. deleteImage: Please select an image to delete.
deleteSuccess: "{name} deleted successfully." deleteSuccess: "{name} deleted successfully."
imagePreloadStrategy: Image Preload Strategy imagePreloadStrategy: Image Preload Strategy
nodeUpgradeOption: Node Upgrade Option
restoreVM: Restore VM restoreVM: Restore VM
strategy: Strategy
pauseNodes: Pause Nodes
strategyType: Strategy Type strategyType: Strategy Type
concurrency: Concurrency concurrency: Concurrency
harvesterMonitoring: harvesterMonitoring: