Compare commits

..
19 Commits
Author SHA1 Message Date
mergify[bot]andAndy Lee 19b6842b21 chore: bump version to v1.6.0 (#480) (#481)
(cherry picked from commit d175e3b11a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-26 11:22:02 +08:00
mergify[bot]andAndy Lee fa78229d3a fix: resource page detail title link (#459) (#464)
* fix: resource page detail title link



* refactor: extract harvester type in a map



* refactor: put in config/harvester-map.js



---------


(cherry picked from commit 795e5d178f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 16:54:00 +08:00
mergify[bot]andAndy Lee c3bfaf00ea fix: spec.domain.memory.guest is lost issue (#462) (#463)
(cherry picked from commit 2e03ac3cf7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 12:02:18 +08:00
mergify[bot]andAndy Lee 1a176960bc refactor: remove unneeded divider in storage class action menu (#460) (#461)
(cherry picked from commit 76605fdc07)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 10:53:56 +08:00
mergify[bot]andAndy Lee e82ba9d9f5 chore: bump version to v1.6.0-rc6 (#456) (#457)
(cherry picked from commit c289b001db)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-13 15:16:55 +08:00
mergify[bot]andYiya Chen f7fc001abd fix: fallback default value (#454) (#455)
(cherry picked from commit e8d63da1eb)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-08-12 13:55:37 +08:00
mergify[bot]andAndy Lee d1c676ee2c chore: update reviewer (#452) (#453)
(cherry picked from commit 4a456ac07d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-05 12:41:13 +08:00
mergify[bot]andAndy Lee c562dd73ba chore: bump to 1.6.0-rc5 (#450) (#451)
(cherry picked from commit 57dfddd593)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:34:33 +08:00
mergify[bot]andAndy Lee 1cd5e5ca7d feat: add restore failed message in VM state description (#448) (#449)
(cherry picked from commit 66eac96575)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:11:58 +08:00
mergify[bot]andAndy Lee 1aaa8f71c4 fix: featureFlags is undefined when installing lower version of ui-ext (#443) (#447)
* fix: featureFlags is undefined if install lower version of ui-ext

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

(cherry picked from commit 182d92d80b)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:41:10 +08:00
mergify[bot]andYiya Chen 003a09d0a9 fix: update cdiSettings (#444) (#445)
(cherry picked from commit 1b362f570b)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-08-01 12:15:10 +08:00
mergify[bot]andAndy Lee f08e632aad chore: bump version to v1.6.0-rc4 (#439) (#442)
* chore: bump version to v1.6.0-rc4



* fix: missing vm network migration setting



---------


(cherry picked from commit cf798048f8)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:07:47 +08:00
mergify[bot]andAndy Lee df1d7001bc fix: cpu/ memory columns in host detail VM tab (#436) (#437)
(cherry picked from commit 93c8399935)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 11:57:52 +08:00
mergify[bot]andAndy Lee 41f78c83a7 fix: isStopping state condition (#431) (#435)
* fix: isStopping state condition



* refactor: remove unused variable



---------


(cherry picked from commit b72f523ddf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 13:02:30 +08:00
mergify[bot]andAndy Lee f1a53311ce chore: upgrade rancher/shell to 3.0.5-rc.7 (#429) (#432)
(cherry picked from commit e5ffb08df3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 10:00:10 +08:00
mergify[bot]andAndy Lee e0c6e905a9 refactor: filter anchor link in setting description when searching (#428) (#430)
(cherry picked from commit 98a6322c11)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:26:19 +08:00
mergify[bot]andAndy Lee 1852e434f1 feat: add runStragety field when cloning a VM (#424) (#427)
* feat: add runStragety field when cloning a VM



* refactor: remove unneeded change



---------


(cherry picked from commit be7e4bd80b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:31:18 +08:00
mergify[bot] 77c085d3a7 feat: expose CDI settings (#418) (#425)
* feat: add cdi settings tab



* feat: basic layout



* feat: add fields mutation



* refactor: rename keys



* refactor: add edit mode



* refactor: remove isCreate



* feat: filter volume mode options



---------

(cherry picked from commit ed2bc3100b)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-25 13:59:50 +08:00
mergify[bot]andYiya Chen 9dbe7d9015 fix: exception error (#422) (#423)
(cherry picked from commit b6ffb3e9f1)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-24 13:00:44 +08:00
33 changed files with 635 additions and 225 deletions
-1
View File
@@ -4,4 +4,3 @@ numberOfReviewers: 0
reviewers: reviewers:
- a110605 - a110605
- houhoucoop - houhoucoop
- NickChungSUSE
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.6.0-rc3", "version": "1.6.0",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.26.0", "@babel/plugin-transform-class-static-block": "7.26.0",
"@rancher/shell": "3.0.5-rc.6", "@rancher/shell": "3.0.5-rc.7",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"color": "4.2.3", "color": "4.2.3",
"ip": "2.0.1", "ip": "2.0.1",
+5 -1
View File
@@ -98,8 +98,12 @@ export default {
return true; return true;
} }
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || ''; let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
// remove <a> tags from description
if (description.includes('<a')) {
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
}
// filter by description // filter by description
if (description.includes(searchQuery)) { if (description.includes(searchQuery)) {
return true; return true;
@@ -53,8 +53,6 @@ export default {
}, },
namespaceOptions() { namespaceOptions() {
if (this.availableNamespaces.length === 0) return [];
const allSelected = const allSelected =
this.namespaces.length === this.filteredNamespaces.length && this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns)); this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
+3 -1
View File
@@ -45,7 +45,9 @@ const FEATURE_FLAGS = {
'vmNetworkMigration', 'vmNetworkMigration',
'kubeovnVpcSubnet', 'kubeovnVpcSubnet',
'rancherClusterSetting', 'rancherClusterSetting',
'cpuMemoryHotplug' 'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
] ]
}; };
+13
View File
@@ -1,3 +1,5 @@
import { HCI } from '../types';
// TODO: delete this not used variable // TODO: delete this not used variable
export const MemoryUnit = [{ export const MemoryUnit = [{
label: 'Mi', label: 'Mi',
@@ -81,3 +83,14 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace', CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
}; };
// Some harvester CRD type is not equal to model file name, define the mapping here
export const HARVESTER_CRD_MAP = {
node: HCI.HOST,
configmap: HCI.CLOUD_TEMPLATE,
persistentvolumeclaim: HCI.VOLUME,
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
// specific groupable table detail page
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
'kubeovn.io.subnet': HCI.VPC
};
@@ -72,4 +72,8 @@ export const HCI = {
CUSTOM_IP: 'harvesterhci.io/custom-ip', CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported', IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug', VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
}; };
+4 -1
View File
@@ -113,7 +113,10 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.RANCHER_CLUSTER]: { [HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting' kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
}, },
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' }, [HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
}; };
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
@@ -47,20 +47,26 @@ export default {
STATE, STATE,
NAME, NAME,
{ {
name: 'vmCPU', name: 'CPU',
labelKey: 'tableHeaders.cpu', label: 'CPU',
search: false, sort: ['displayCPU'],
sort: ['spec.template.spec.domain.cpu.cores'], value: 'displayCPU',
value: 'spec.template.spec.domain.cpu.cores', align: 'center',
width: 120 dashIfEmpty: true,
}, },
{ {
name: 'vmRAM', name: 'Memory',
labelKey: 'glance.memory', value: 'displayMemory',
search: false, sort: ['memorySort'],
sort: ['memorySort'], align: 'center',
value: 'spec.template.spec.domain.resources.limits.memory', labelKey: 'tableHeaders.memory',
width: 120 formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
}, },
{ {
name: 'ip', name: 'ip',
+20 -6
View File
@@ -1,12 +1,13 @@
<script> <script>
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'CloneVMModal', name: 'CloneVMModal',
@@ -14,7 +15,7 @@ export default {
emits: ['close'], emits: ['close'],
components: { components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
}, },
props: { props: {
@@ -26,9 +27,11 @@ export default {
data() { data() {
return { return {
name: '', name: '',
cloneData: true, cloneData: true,
errors: [] errors: [],
runStrategy: runStrategyOptions[1],
runStrategyOptions
}; };
}, },
@@ -38,6 +41,9 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
}, },
methods: { methods: {
@@ -58,7 +64,8 @@ export default {
// deep clone // deep clone
try { try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false); const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
const res = await this.actionResource.doAction('clone', payload, {}, false);
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
@@ -106,6 +113,13 @@ export default {
:label="t('harvester.modal.cloneVM.name')" :label="t('harvester.modal.cloneVM.name')"
required required
/> />
<LabeledSelect
v-if="vmCloneRunStrategyEnabled"
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="runStrategyOptions"
:mode="mode"
/>
<Banner <Banner
v-for="(err, i) in errors" v-for="(err, i) in errors"
:key="i" :key="i"
-149
View File
@@ -1,149 +0,0 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,319 @@
<script>
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import ArrayList from '@shell/components/form/ArrayList';
import { Checkbox } from '@components/Form/Checkbox';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
import { HCI_SETTING } from '../../config/settings';
import { allHash } from '@shell/utils/promise';
import { _EDIT, _CREATE } from '@shell/config/query-params';
export default {
name: 'CDISettings',
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
provisioner: {
type: String,
required: true
},
},
components: {
ArrayList,
Checkbox,
LabeledInput,
LabeledSelect,
},
emits: ['update:cdiSettings'],
async fetch() {
const hash = {
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
};
await allHash(hash);
if (this.mode === _CREATE ) {
this.setDefaultVolumeSnapshotClass();
} else {
this.initCDISettingsFromAnnotations();
}
},
data() {
return {
cdiSettings: {
volumeModeAccessModes: [],
volumeSnapshotClass: null,
cloneStrategy: null,
filesystemOverhead: null,
},
defaultAddValue: { volumeMode: null, accessModes: [] },
noneOption: { label: 'None', value: '' },
};
},
computed: {
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
allVolumeModeOptions() {
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
},
selectedVolumeModes() {
return this.cdiSettings.volumeModeAccessModes
.map((item) => item.volumeMode)
.filter(Boolean);
},
volumeModeOptions() {
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
},
allVolumeModesSelected() {
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
},
accessModeOptions() {
return [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
];
},
cloneStrategyOptions() {
return [
this.noneOption,
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
];
},
volumeSnapshotClassOptions() {
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
},
isFilesystemOverheadValid() {
const val = this.cdiSettings.filesystemOverhead;
if (val === null || val === '') return true;
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
return regex.test(val);
},
isCustomClass() {
return this.mode === _CREATE || this.mode === _EDIT;
}
},
watch: {
provisioner() {
this.resetCdiSettings();
this.$nextTick(this.setDefaultVolumeSnapshotClass);
},
cdiSettings: {
handler(val) {
this.$emit('update:cdiSettings', val);
},
deep: true,
immediate: true,
}
},
methods: {
setDefaultVolumeSnapshotClass() {
try {
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
const config = JSON.parse(setting?.value || setting?.default || '{}');
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
} catch (e) {
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
this.cdiSettings.volumeSnapshotClass = null;
}
},
initCDISettingsFromAnnotations() {
const annotations = this.value?.metadata?.annotations || {};
let volumeModeAccessModes = [];
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
if (rawVolumeMode) {
try {
const parsed = JSON.parse(rawVolumeMode);
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
volumeMode,
accessModes: Array.isArray(accessModes) ? accessModes : [],
}));
} catch (e) {
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
}
}
if (volumeModeAccessModes.length) {
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
}
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
},
resetCdiSettings() {
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
this.cdiSettings.volumeSnapshotClass = null;
this.cdiSettings.cloneStrategy = null;
this.cdiSettings.filesystemOverhead = null;
},
validateFilesystemOverhead(e) {
const val = e.target.value;
this.cdiSettings.filesystemOverhead = val;
}
}
};
</script>
<template>
<ArrayList
v-model:value="cdiSettings.volumeModeAccessModes"
:initial-empty-row="true"
:show-header="true"
:mode="mode"
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
:default-add-value="defaultAddValue"
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
:add-disabled="allVolumeModesSelected"
>
<template #column-headers>
<div class="column-headers">
<div
class="row"
:class="{ custom: isCustomClass }"
>
<label
class="col span-3 value text-label mb-10"
for="volumeMode"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
</label>
<label
class="col span-9 value text-label mb-10"
for="accessModes"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
</label>
</div>
</div>
</template>
<template #columns="{ row }">
<div class="row">
<div class="col span-3">
<LabeledSelect
id="volumeMode"
v-model:value="row.value.volumeMode"
:mode="mode"
:options="volumeModeOptions"
/>
</div>
<div
id="accessModes"
class="col span-9"
>
<Checkbox
v-for="opt in accessModeOptions"
:key="opt.value"
v-model:value="row.value.accessModes"
:value-when-true="opt.value"
:label="opt.label"
type="checkbox"
:mode="mode"
/>
</div>
</div>
</template>
</ArrayList>
<LabeledSelect
v-model:value="cdiSettings.volumeSnapshotClass"
class="select mt-20 mb-20"
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
:mode="mode"
:options="volumeSnapshotClassOptions"
/>
<LabeledSelect
v-model:value="cdiSettings.cloneStrategy"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
:mode="mode"
:options="cloneStrategyOptions"
/>
<LabeledInput
v-model:value="cdiSettings.filesystemOverhead"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
:mode="mode"
type="number"
:min="0"
:max="1"
:step="0.001"
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
:status="isFilesystemOverheadValid ? null : 'error'"
@input="validateFilesystemOverhead"
/>
</template>
<style scoped lang="scss">
.column-headers .row.custom {
max-width: calc(100% - 75px);
}
.row {
align-items: center;
}
.select {
max-width: 480px;
}
</style>
@@ -1,4 +1,5 @@
<script> <script>
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import { _CREATE, _VIEW } from '@shell/config/query-params'; import { _CREATE, _VIEW } from '@shell/config/query-params';
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features'; import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
import { import {
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import CDISettings from './CDISettings';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node'; export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
@@ -45,6 +45,7 @@ export default {
Tabbed, Tabbed,
Loading, Loading,
Tags, Tags,
CDISettings,
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@@ -117,6 +118,7 @@ export default {
key: '', key: '',
values: [], values: [],
}, },
cdiSettings: {},
}; };
}, },
@@ -124,10 +126,10 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const hash = { const hash = {
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }), namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }), longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
}; };
if (this.value.longhornV2LVMSupport) { if (this.value.longhornV2LVMSupport) {
@@ -197,6 +199,14 @@ export default {
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1; return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
}, },
isCDISettingsFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
},
shouldShowCDISettingsTab() {
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
},
}, },
watch: { watch: {
@@ -249,6 +259,10 @@ export default {
}); });
this.formatAllowedTopoloties(); this.formatAllowedTopoloties();
if (this.shouldShowCDISettingsTab) {
this.formatCDISettings();
}
}, },
formatAllowedTopoloties() { formatAllowedTopoloties() {
@@ -270,6 +284,42 @@ export default {
if (matchLabelExpressions.length > 0) { if (matchLabelExpressions.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }]; this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
} }
},
formatCDISettings() {
const annotations = this.value.metadata.annotations || {};
const volumeModeAccessModes = {};
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
}
});
if (Object.keys(volumeModeAccessModes).length > 0) {
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
}
if (this.cdiSettings.volumeSnapshotClass) {
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (this.cdiSettings.cloneStrategy) {
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
} else {
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (this.cdiSettings.filesystemOverhead) {
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
} else {
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
this.value.metadata.annotations = annotations;
} }
} }
}; };
@@ -404,6 +454,19 @@ export default {
</template> </template>
</ArrayList> </ArrayList>
</Tab> </Tab>
<Tab
v-if="shouldShowCDISettingsTab"
name="cdiSettings"
:label="t('harvester.storage.cdiSettings.label')"
:weight="-2"
>
<CDISettings
v-model:cdi-settings="cdiSettings"
:value="value"
:mode="mode"
:provisioner="value.provisioner"
/>
</Tab>
</Tabbed> </Tabbed>
</CruResource> </CruResource>
</template> </template>
@@ -257,6 +257,7 @@ export default {
:label="t('harvester.vmTemplate.tabs.basics')" :label="t('harvester.vmTemplate.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:memory="memory" :memory="memory"
:max-cpu="maxCpu" :max-cpu="maxCpu"
@@ -240,7 +240,7 @@ export default {
</script> </script>
<template> <template>
<div class="mt-20"> <div>
<LabeledSelect <LabeledSelect
v-model:value="checkedSsh" v-model:value="checkedSsh"
:label="t('harvester.virtualMachine.input.sshKey')" :label="t('harvester.virtualMachine.input.sshKey')"
@@ -605,6 +605,7 @@ export default {
:label="t('harvester.virtualMachine.detail.tabs.basics')" :label="t('harvester.virtualMachine.detail.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:max-cpu="maxCpu" :max-cpu="maxCpu"
:memory="memory" :memory="memory"
+22 -2
View File
@@ -1080,8 +1080,6 @@ harvester:
placeholder: e.g. 169.254.0.1/16 placeholder: e.g. 169.254.0.1/16
remoteVpc: remoteVpc:
label: Remote VPC label: Remote VPC
network: network:
label: Virtual Machine Networks label: Virtual Machine Networks
tabs: tabs:
@@ -1388,6 +1386,28 @@ harvester:
label: Internal Storage Class label: Internal Storage Class
cannotDeleteTooltip: Internal storage class volumes cannot be deleted cannotDeleteTooltip: Internal storage class volumes cannot be deleted
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
cdiSettings:
label: CDI Settings
volumeModeAccessModes:
label: Volume Mode / Access Modes
tooltip: Specifies the default volume mode and access modes.
add: Add
volumeMode: Volume Mode
accessModes:
label: Access Modes
volumeSnapshotClass:
label: Volume Snapshot Class
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
cloneStrategy:
label: Clone Strategy
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
copy: Copy
snapshot: Snapshot
csiClone: CSI Clone
fileSystemOverhead:
label: File System Overhead
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
vlanConfig: vlanConfig:
title: Network Configuration title: Network Configuration
+7 -4
View File
@@ -691,10 +691,13 @@ export default {
this.spec.template.spec.domain.cpu.cores = this.cpu; this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString(); this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
this.spec.template.spec.domain.resources.limits.memory = this.memory; this.spec.template.spec.domain.resources.limits.memory = this.memory;
// clean // clean below fields as we don't need them if not enable CPU and memory hotplug
delete this.spec.template.spec.resources; if (this.spec?.template?.spec?.domain?.cpu?.maxSockets) {
delete this.spec.template.spec.domain.memory; delete this.spec.template.spec.domain.cpu.maxSockets;
delete this.spec.template.spec.domain.cpu.maxSockets; }
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
delete this.spec.template.spec.domain.memory.maxGuest;
}
} }
}, },
+3 -1
View File
@@ -1,17 +1,19 @@
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import { VIEW_IN_API, DEV } from '@shell/store/prefs'; import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
export default class HarvesterResource extends SteveModel { export default class HarvesterResource extends SteveModel {
get listLocation() { get listLocation() {
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`; const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || { return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
name, name,
params: { params: {
product: this.$rootGetters['productId'], product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'], cluster: this.$rootGetters['clusterId'],
resource: this.type, resource
}, },
}; };
} }
@@ -1,3 +1,4 @@
// This file is Cloud Configuration Templates page model
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { HCI } from '../../types'; import { HCI } from '../../types';
import HarvesterResource from '../harvester'; import HarvesterResource from '../harvester';
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
params: { params: {
product: HARVESTER_PRODUCT, product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'], cluster: this.$rootGetters['clusterId'],
resource: this.type, resource: HCI.NETWORK_ATTACHMENT,
}, },
}; };
} }
@@ -9,6 +9,19 @@ export default class HciClusterflow extends HarvesterFlow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || []; return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -21,6 +21,19 @@ export default class HciClusteroutput extends LogOutput {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -14,6 +14,19 @@ export default class HarvesterLogFlow extends Flow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || []; return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -22,6 +22,19 @@ export default class HarvesterLogOutput extends LogOutput {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -19,6 +19,19 @@ export default class HciAlertmanagerConfig extends SteveModel {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get doneOverride() { get doneOverride() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
+13
View File
@@ -5,6 +5,19 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret { export default class HciSecret extends Secret {
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.SECRET;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -9,6 +9,19 @@ import { isInternalStorageClass } from '../../utils/storage-class';
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io'; export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
export default class HciStorageClass extends StorageClass { export default class HciStorageClass extends StorageClass {
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.STORAGE;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -94,13 +107,14 @@ export default class HciStorageClass extends StorageClass {
get availableActions() { get availableActions() {
let out = super.availableActions || []; let out = super.availableActions || [];
out = out.map((action) => { if (this.isInternalStorageClass()) {
if ((action.action === 'setDefault' || action.action === 'setAsDefault' || action.action === 'promptRemove') && this.isInternalStorageClass()) { out = out.filter((action) => {
return { ...action, enabled: false }; return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
} });
}
return action; if (out[0] && out[0].divider === true) {
}); out = out.slice(1);
}
return out; return out;
} }
@@ -25,6 +25,10 @@ export default class HciVmBackup extends HarvesterResource {
return detailLocation; return detailLocation;
} }
get disableResourceDetailDrawerConfigTab() {
return true;
}
get doneOverride() { get doneOverride() {
const route = this.currentRoute(); const route = this.currentRoute();
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -32,8 +32,6 @@ const PAUSED_VM_MODAL_MESSAGE =
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE'; const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING'; const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
// eslint-disable-next-line no-unused-vars
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
const POD_STATUS_FAILED = 'POD_FAILED'; const POD_STATUS_FAILED = 'POD_FAILED';
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF'; const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
@@ -742,6 +740,7 @@ export default class VirtVm extends HarvesterResource {
if (this && if (this &&
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
this.isVMCreated && this.isVMCreated &&
this.vmi?.status?.phase !== undefined &&
this.vmi?.status?.phase !== VMIPhase.Succeeded && this.vmi?.status?.phase !== VMIPhase.Succeeded &&
this.vmi?.status?.phase !== VMIPhase.Pending this.vmi?.status?.phase !== VMIPhase.Pending
) { ) {
@@ -1165,6 +1164,13 @@ export default class VirtVm extends HarvesterResource {
return this.t('harvester.virtualMachine.hotplug.restartVMMessage'); return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
} }
const restoreConditions = this.restoreResource?.status?.conditions || [];
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
if (restoreError?.message) {
return restoreError.message;
}
return this.ingoreVMMessage ? '' : super.stateDescription; return this.ingoreVMMessage ? '' : super.stateDescription;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.6.0-rc3", "version": "1.6.0",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
+17 -13
View File
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
}; };
export function getVersion(v) { export function getVersion(v) {
// e.g v1.4.0
if (process.env.VUE_APP_SERVER_VERSION) { if (process.env.VUE_APP_SERVER_VERSION) {
return process.env.VUE_APP_SERVER_VERSION; return process.env.VUE_APP_SERVER_VERSION;
} }
@@ -18,20 +17,21 @@ export function getVersion(v) {
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`; return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
} catch (error) { } catch (error) {
// fallback to the latest version // fallback to the latest version
return latestVersion(Object.keys(RELEASE_FEATURES)); return getLatestVersion();
} }
} }
function latestVersion(versions) { function getLatestVersion() {
return versions.sort((a, b) => semver.compare(a, b)).pop(); const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
return allVersions[0] || '';
} }
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x function getLatestCompatibleVersion(version) {
function latestMinorVersion(v) { const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`; const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
return latestVersion(minorVersions); return compatible[0] || getLatestVersion();
} }
export const featureEnabled = (featureKey, serverVersion) => { export const featureEnabled = (featureKey, serverVersion) => {
@@ -46,12 +46,16 @@ export const featureEnabled = (featureKey, serverVersion) => {
return false; return false;
} }
let releasedFeatures = RELEASE_FEATURES[version]; const compatibleVersion = getLatestCompatibleVersion(version);
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
if (!releasedFeatures) { if (!Array.isArray(releasedFeatures)) {
const fallback = latestMinorVersion(version); // eslint-disable-next-line no-console
console.error(
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
);
releasedFeatures = RELEASE_FEATURES[fallback]; return false;
} }
return releasedFeatures.includes(featureKey); return releasedFeatures.includes(featureKey);
+9 -9
View File
@@ -3133,15 +3133,15 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rancher/icons@2.0.29": "@rancher/icons@2.0.37":
version "2.0.29" version "2.0.37"
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a" resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.37.tgz#61dd947009a4a3d27b2e4dca630da05583925744"
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA== integrity sha512-T2FqiZraz9yte9tNMCGqIDQVPwXKTAPUTE8eAaQjhOJEbwfk/ttXDIKGYZ9mHApJX6s8JYVXAbRPcgXgc4zKpA==
"@rancher/shell@3.0.5-rc.6": "@rancher/shell@3.0.5-rc.7":
version "3.0.5-rc.6" version "3.0.5-rc.7"
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.6.tgz#8d842cf1b0f7e3e52af53538953cf9dc5bfd0639" resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.7.tgz#a3c3f555c9459b312546fb577e9916cd48126fb2"
integrity sha512-B9+2WE1a06wRvCN7xvxp/HBdjrib04szIbc1pNBeMdDNCqQmZMvt2dftjYb44Tvg+4Ji8G+tcK8Q3Rry6NDJsw== integrity sha512-YxiRteWwTnuCGEh2AqIyH6Vx/PJXSwugi7dpf2U+XnQLqQHYM8pFo38jG8Tmj95KqZsRMLXdesRow/Icq96D5w==
dependencies: dependencies:
"@aws-sdk/client-ec2" "3.658.1" "@aws-sdk/client-ec2" "3.658.1"
"@aws-sdk/client-eks" "3.1.0" "@aws-sdk/client-eks" "3.1.0"
@@ -3153,7 +3153,7 @@
"@babel/preset-typescript" "7.16.7" "@babel/preset-typescript" "7.16.7"
"@novnc/novnc" "1.2.0" "@novnc/novnc" "1.2.0"
"@popperjs/core" "2.11.8" "@popperjs/core" "2.11.8"
"@rancher/icons" "2.0.29" "@rancher/icons" "2.0.37"
"@types/is-url" "1.2.30" "@types/is-url" "1.2.30"
"@types/node" "20.10.8" "@types/node" "20.10.8"
"@types/semver" "^7.5.8" "@types/semver" "^7.5.8"