Compare commits

..
20 Commits
Author SHA1 Message Date
Andy Lee 3c464e9d10 bump version to v1.5.1-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-28 13:24:25 +08:00
Andy Lee d412ee2aa6 fix conflict
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-28 13:05:55 +08:00
mergify[bot]andAndy Lee 580b2892cb fix upgrade button status if have errors (#316) (#321)
(cherry picked from commit cb452b9627)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-28 13:00:40 +08:00
mergify[bot]andAndy Lee 965c7d9b72 Add OS upgrade features (backport #311) (#320)
* Add OS upgrade features (#311)

* Add failed and success banner after image uploaded

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

* add delete image feature

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

* add skip checking single-replica detached volumes checkbox

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

* change delete image flow

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

* Reuse ConfirmRelatedToRemoveDialog

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit dbbad01b0f)

# Conflicts:
#	pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue

* resolve conflict

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-28 12:58:46 +08:00
mergify[bot]andAndy Lee 01e3867da1 give default valeu for uploadImage opt (#308) (#319)
(cherry picked from commit fe32b4372b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-28 12:49:06 +08:00
mergify[bot]andAndy Lee fd52218350 Fix OS upgrade page (#306) (#318)
* fix OS upgrade page



* move uploadFile action



* change os upgrage ui to standard feature



* Add uploadImage cancel



---------



(cherry picked from commit 6d8f6579c7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: a110605 <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-28 12:46:02 +08:00
Andy Lee 1839bf387e fix clone VM dialog error position
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-28 12:38:13 +08:00
mergify[bot]andAndy Lee 3e9a0a4377 Refactor feature flags structure (backport #276) (#314)
* Refactor feature flags structure (#276)

* refactor feature flags structure

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

* Replace with FEATURE_FLAGS and valid and sort versions

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit f8079c5924)

# Conflicts:
#	pkg/harvester/config/feature-flags.js

* resolve conflict

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-26 15:19:28 +08:00
mergify[bot]andAndy Lee a2898a2554 Change isKVMDisable condition (#294) (#304)
* change condition to check isKVMDisable



* change condition eq 0



* add allNotExist back



---------


(cherry picked from commit aa9dd32cce)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-19 10:19:43 +08:00
mergify[bot]andYiya Chen d11a045aed fix: range condition (#299) (#301)
(cherry picked from commit cbfcf4dbae)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-05-15 16:26:23 +08:00
mergify[bot] ae2a2f6d28 upgrade shell to 3.0.5-rc.2 (#297) (#298) 2025-05-15 11:25:46 +08:00
mergify[bot]andAndy Lee cf68c3976a Upgrade rancher/shell to 3.0.5-rc1 (backport #282) (#296)
* upgrade shell to 3.0.5-rc1 (#282)

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 0cd5651c38)

# Conflicts:
#	yarn.lock

* solve conflict in yarn.lock

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-14 16:59:14 +08:00
Andy Lee 33f8300c0e Bump version to 1.5.1-dev
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-14 16:45:23 +08:00
renovate[bot] b26e34a2bd chore(deps): update dependency follow-redirects to v1.15.6 [security] (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 03:11:21 +00:00
renovate[bot] 01fd3e5e61 chore(deps): update dependency ejs to v3.1.10 [security] (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 03:08:36 +00:00
mergify[bot]andAndy Lee d5df7ba602 Remove unneeded condition (#279) (#280)
(cherry picked from commit 8410f75859)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:50:46 +08:00
mergify[bot]andAndy Lee 607aa106e1 fix #cloud-config disappear when add/remove ssh key (#273) (#277) (#278)
(cherry picked from commit 9dd8ae479a)



(cherry picked from commit 1e270268d8)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:38:58 +08:00
mergify[bot]andYiya Chen 49e2be6ff4 fix: skip escapehtml (#272) (#275)
(cherry picked from commit 9dd36bbd87)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-05-08 15:55:19 +08:00
mergify[bot]andYiya Chen 9ee81f4054 fix: hide btns before is ready (#267) (#268)
(cherry picked from commit 5841508b26)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-05-06 11:47:46 +08:00
mergify[bot]andAndy Lee f8fa82f614 add require backport 1.5 label in mergify.yml (#265) (#266)
(cherry picked from commit 485db3066f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-28 11:46:39 +08:00
20 changed files with 618 additions and 1260 deletions
+1
View File
@@ -2,6 +2,7 @@ pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
- label="require backport 1.5"
actions:
backport:
branches:
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "harvester-ui-extension",
"version": "1.5.0",
"version": "1.5.1-rc1",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@rancher/shell": "3.0.2-rc.6",
"@rancher/shell": "3.0.5-rc.2",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
@@ -19,8 +19,8 @@
"@types/node": "~20.10.0",
"cronstrue": "2.50.0",
"d3-color": "3.1.0",
"ejs": "3.1.9",
"follow-redirects": "1.15.2",
"ejs": "3.1.10",
"follow-redirects": "1.15.6",
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
+1 -1
View File
@@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p>
<p class="mb-5">
<p>
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion"
target="_blank"
+53 -67
View File
@@ -1,69 +1,55 @@
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
const featuresV130 = [
'supportHarvesterClusterVersion'
];
import semver from 'semver';
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
const featuresV131 = [
...featuresV130,
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
];
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
const featuresV132 = [
...featuresV131,
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
];
// TODO: add v1.3.3 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
const featuresV133 = [
...featuresV132,
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
const featuresV140 = [
...featuresV133,
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
const featuresV141 = [
...featuresV140
];
// TODO: add v1.4.2 official release note
const featuresV142 = [
...featuresV141,
'refreshIntervalInSecond',
'allowEmptySnapshotClassName'
];
// TODO: add v1.5.0 official release note
const featuresV150 = [
...featuresV142,
'tpmPersistentState',
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol',
'thirdPartyStorage'
];
export const RELEASE_FEATURES = {
'v1.3.0': featuresV130,
'v1.3.1': featuresV131,
'v1.3.2': featuresV132,
'v1.3.3': featuresV133,
'v1.4.0': featuresV140,
'v1.4.1': featuresV141,
'v1.4.2': featuresV142,
'v1.5.0': featuresV150
const FEATURE_FLAGS = {
'v1.3.0': [
'supportHarvesterClusterVersion'
],
'v1.3.1': [
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
],
'v1.3.2': [
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
],
'v1.3.3': [],
'v1.4.0': [
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
],
'v1.4.1': [],
'v1.4.2': [
'refreshIntervalInSecond',
'allowEmptySnapshotClassName'
],
'v1.4.3': [],
'v1.5.0': [
'tpmPersistentState',
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol',
'thirdPartyStorage'
],
'v1.5.1': [],
};
const generateFeatureFlags = () => {
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
const generatedFlags = {};
versions.forEach((version, index) => {
const previousVersion = versions[index - 1];
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
});
return generatedFlags;
};
export const RELEASE_FEATURES = generateFeatureFlags();
@@ -66,4 +66,5 @@ export const HCI = {
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
K8S_ARCH: 'kubernetes.io/arch'
};
+6 -5
View File
@@ -106,6 +106,12 @@ export default {
:label="t('harvester.modal.cloneVM.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
@@ -127,11 +133,6 @@ export default {
@click="create"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template>
</Card>
</template>
@@ -67,7 +67,7 @@ export default {
plusMore() {
const remaining = this.resources.length - this.names.length;
return this.t('dialog.confirmExecution.andOthers', { count: remaining });
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
},
type() {
@@ -42,8 +42,25 @@ export default {
computed: {
...mapState('action-menu', ['modalData']),
warningMessageKey() {
return this.modalData.warningMessageKey;
title() {
return this.modalData.title || 'dialog.promptRemove.title';
},
formattedType() {
return this.type.toLowerCase();
},
warningMessage() {
if (this.modalData.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
type: this.formattedType,
thisOrThese,
});
return defaultMessage;
},
names() {
@@ -70,6 +87,12 @@ export default {
return this.resources[0].nameDisplay;
},
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() {
const remaining = this.resources.length - this.names.length;
@@ -97,6 +120,10 @@ export default {
},
deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch;
},
@@ -144,24 +171,32 @@ export default {
<template #body>
<div class="pl-10 pr-10">
<span
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
v-clean-html="warningMessage"
></span>
<div class="mt-10 mb-10">
<span
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
<div
v-if="needConfirmation"
class="mt-20"
>
<div class="mt-10 mb-10">
<span
v-clean-html="t('dialog.promptRemove.confirmName', {
type: formattedType,
nameToMatch: escapeHtml(nameToMatch)
}, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
</div>
</div>
<Banner
v-for="(error, i) in errors"
@@ -290,7 +290,7 @@ export default {
const res = await this.value.save();
res.uploadImage(file);
await res.uploadImage(file);
buttonCb(true);
this.done();
@@ -6,7 +6,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard';
import { _VIEW, _EDIT } from '@shell/config/query-params';
import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types';
@@ -90,9 +90,6 @@ export default {
},
sshOption() {
if (this.mode === _VIEW || this.mode === _EDIT) {
return [];
}
const out = this.$store.getters['harvester/all'](HCI.SSH).map( (O) => {
return {
label: O.id,
@@ -29,7 +29,7 @@ export default {
const rows = (this.value || []).map((row) => {
let type = 'cidr';
if (row.rangeStart && row.rangeEnd) {
if (row.rangeStart || row.rangeEnd) {
type = 'range';
}
+17 -2
View File
@@ -61,6 +61,7 @@ harvester:
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
productLabel: 'Harvester'
modal:
backup:
success: 'Backup { backUpName } has been initiated.'
addBackup: Add Backup
@@ -99,6 +100,9 @@ harvester:
tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!'
osImage:
title: Delete Image
message: Are you sure you want to delete the image { name } ?
downloadImage:
title: Download Image
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
@@ -842,6 +846,8 @@ harvester:
=1 {1 image is uploading, please do not refresh or close the page.}
other {{count} images are uploading, please do not refresh or close the page.}
}
osUpgrade:
uploading: "{name} is uploading, please do not refresh or close the page."
checksum: Checksum
checksumTip: Validate the image using the SHA512 checksum, if specified.
@@ -870,14 +876,18 @@ harvester:
upgradeNode: Upgrading Node
upgradeSysService: Upgrading System Service
upgradeImage: Download Upgrade Image
osUpgrade: OS Upgrade
osUpgrade: Cluster Upgrade
uploadNew: Upload New Image
deleteHeader: Please select an image to delete.
deleteExisting: Delete Existing Image
selectExisting: Select Existing Image
createRepository: Creating Upgrade Repository
succeeded: Succeeded
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
pending: Pending
upload:
duplicatedFile: The file you are trying to upload already exists.
repoInfo:
upgradeStatus: Upgrade Status
os: OS
@@ -1080,10 +1090,15 @@ harvester:
addConfig: Add Configuration
upgrade:
selectExitImage: Please select the OS image to upgrade.
unknownImageName: Image name is not found.
imageUrl: Please input a valid image URL.
chooseFile: Please select to upload an image.
checksum: Checksum
networkError: Unable to upload the image. Resolve network issues that may have occurred and try again.
cancelUpload: Cancelled the image upload.
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
deleteImage: Please select an image to delete.
deleteSuccess: "{name} deleted successfully."
harvesterMonitoring:
label: Harvester Monitoring
section:
+2 -11
View File
@@ -2,13 +2,12 @@
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue';
import Settings from '@pkg/harvester/components/SettingList.vue';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
import { HCI } from '../types';
export default {
@@ -21,14 +20,6 @@ export default {
},
async fetch() {
let isDev;
try {
isDev = this.$store.getters['prefs/get'](VIEW_IN_API);
} catch {
isDev = this.$store.getters['prefs/get'](DEV);
}
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -77,7 +68,7 @@ export default {
};
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
s.hasActions = !s.readOnly || isDev;
s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly;
initSettings.push(s);
});
+9 -17
View File
@@ -922,7 +922,9 @@ export default {
return undefined;
}
return userDataYaml;
const hasCloudComment = this.hasCloudConfigComment(userDataYaml);
return hasCloudComment ? userDataYaml : `#cloud-config\n${ userDataYaml }`;
} catch (e) {
console.error('Error: Unable to parse yaml document', e); // eslint-disable-line no-console
@@ -1443,7 +1445,6 @@ export default {
sshAuthorizedKeys.splice(index, 1);
}
});
const userDataJson = this.convertToJson(this.userScript);
userDataJson.ssh_authorized_keys = sshAuthorizedKeys;
@@ -1579,19 +1580,9 @@ export default {
*/
handler(neu) {
if (this.deleteAgent) {
let out = this.getUserData({
this['userScript'] = this.getUserData({
installAgent: neu, osType: this.osType, deletePackage: this.deletePackage
});
if (neu) {
const hasCloudComment = this.hasCloudConfigComment(out);
if (!hasCloudComment) {
out = `#cloud-config\n${ out }`;
}
}
this['userScript'] = out;
this.refreshYamlEditor();
}
this.deleteAgent = true;
@@ -1616,15 +1607,16 @@ export default {
},
sshKey(neu, old) {
// refresh yaml editor to get the latest userScript
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: this.osType });
this.refreshYamlEditor();
const _diff = difference(old, neu);
// delete removed ssh key in userdata if needed
if (_diff.length > 0 && this.isCreate) {
this.deleteSSHFromUserData(_diff);
}
// refresh yaml editor to get the latest userScript
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: this.osType });
this.refreshYamlEditor();
}
}
};
+8 -1
View File
@@ -186,9 +186,16 @@ export default class HciNode extends HarvesterResource {
}
get isKVMDisable() {
// Arm based
const isARMMachine = this.metadata.labels?.[HCI_ANNOTATIONS.K8S_ARCH]?.includes('arm');
if (isARMMachine) {
return this.status.capacity['devices.kubevirt.io/kvm'] && this.status.capacity['devices.kubevirt.io/kvm'] === '0';
}
const allNotExist = !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] && !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU];
return this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false' || allNotExist;
return allNotExist || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false';
}
get stateDisplay() {
@@ -28,6 +28,7 @@ export default class HciSetting extends HarvesterResource {
}
const schema = this.$getters['schemaFor'](HCI.UPGRADE);
const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase()));
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
@@ -38,7 +39,7 @@ export default class HciSetting extends HarvesterResource {
enabled: true,
icon: 'icon icon-refresh',
label: this.t('harvester.upgradePage.upgrade'),
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded
disabled: !!latestUpgrade && latestUpgrade?.isUpgradeSucceeded
});
}
@@ -50,35 +50,45 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false;
}
return [
const customActions = this.isReady ? [
{
action: 'createFromImage',
enabled: canCreateVM,
icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'),
disabled: !this.isReady,
},
{
action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'),
disabled: !this.isReady,
},
{
action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'),
disabled: !this.isReady,
},
{
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
},
...out
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
}
] : [];
let filteredOut;
if (customActions.length > 0) {
filteredOut = out;
} else {
// if the first item is a divider, remove it from the array
filteredOut = out[0]?.divider ? out.slice(1) : out;
}
return [
...customActions,
...filteredOut
];
}
@@ -295,7 +305,7 @@ export default class HciVmImage extends HarvesterResource {
}
get uploadImage() {
return async(file) => {
return async(file, opt = {}) => {
const formData = new FormData();
formData.append('chunk', file);
@@ -303,22 +313,23 @@ export default class HciVmImage extends HarvesterResource {
try {
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
await this.doAction('upload', formData, {
const result = await this.doAction('upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'File-Size': file.size,
},
params: { size: file.size },
signal: opt.signal,
});
return result;
} catch (err) {
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true });
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
return Promise.reject(err);
throw err;
} finally {
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
}
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.5.0",
"version": "1.5.1-rc1",
"private": false,
"rancher": {
"annotations": {
@@ -7,13 +7,16 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import UpgradeInfo from '../../../../components/UpgradeInfo';
import { HCI } from '../../../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
import { Banner } from '@components/Banner';
import isEmpty from 'lodash/isEmpty';
const IMAGE_METHOD = {
NEW: 'new',
EXIST: 'exist'
NEW: 'new',
EXIST: 'exist',
DELETE: 'delete'
};
const DOWNLOAD = 'download';
@@ -22,7 +25,7 @@ const UPLOAD = 'upload';
export default {
name: 'HarvesterAirgapUpgrade',
components: {
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner
},
inheritAttrs: false,
@@ -39,36 +42,32 @@ export default {
spec: { image: '' }
});
const imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
metadata: {
name: '',
namespace: 'harvester-system',
generateName: 'image-',
annotations: {}
},
spec: {
sourceType: UPLOAD,
displayName: '',
checksum: ''
},
});
await this.initImageValue();
this.value = value;
this.imageValue = imageValue;
},
beforeUnmount() {
if (this.uploadController) {
this.uploadController.abort();
}
},
data() {
return {
value: null,
file: {},
imageId: '',
imageSource: IMAGE_METHOD.NEW,
sourceType: UPLOAD,
imageValue: null,
errors: [],
enableLogging: true,
IMAGE_METHOD
value: null,
file: {},
uploadImageId: '',
imageId: '',
deleteImageId: '',
imageSource: IMAGE_METHOD.NEW,
sourceType: UPLOAD,
uploadController: null,
uploadResult: null,
imageValue: null,
enableLogging: true,
IMAGE_METHOD,
skipSingleReplicaDetachedVol: false,
errors: [],
};
},
@@ -77,22 +76,45 @@ export default {
return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
},
osImageOptions() {
return this.$store.getters['harvester/all'](HCI.IMAGE)
.filter((I) => I.isOSImage)
.map((I) => {
return {
label: I.spec.displayName,
value: I.id,
disabled: !I.isReady
};
});
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
uploadImage() {
allOSImages() {
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
},
deleteOSImageOptions() {
return this.allOSImages.map((I) => {
return {
label: I.spec.displayName,
value: I.id,
};
});
},
osImageOptions() {
return this.allOSImages.map((I) => {
return {
label: I.spec.displayName,
value: I.id,
disabled: !I.isReady
};
});
},
createNewImage() {
return this.imageSource === IMAGE_METHOD.NEW;
},
selectExistImage() {
return this.imageSource === IMAGE_METHOD.EXIST;
},
deleteExistImage() {
return this.imageSource === IMAGE_METHOD.DELETE;
},
fileName() {
return this.file?.name || '';
},
@@ -100,21 +122,89 @@ export default {
canEnableLogging() {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
},
uploadProgress() {
const image = this.$store.getters['harvester/byId'](HCI.IMAGE, this.imageValue.id);
return image?.status?.progress;
},
enableUpgrade() {
if (this.deleteExistImage) {
return false;
}
if (this.sourceType === DOWNLOAD) {
return true;
}
if (this.sourceType === UPLOAD) {
return this.fileName === '' ? true : this.uploadProgress === 100;
}
return true;
},
isUploading() {
return this.fileName !== '' && this.uploadProgress !== 100;
},
showProgressBar() {
return this.createNewImage && this.sourceType === UPLOAD && this.isUploading;
},
showUploadSuccessBanner() {
return this.createNewImage && this.fileName !== '' && isEmpty(this.errors) && !this.showUploadingWarningBanner && this.uploadResult?._status === 200;
},
showUploadingWarningBanner() {
return this.createNewImage && this.isUploading;
},
showUpgradeOptions() {
return this.createNewImage || this.selectExistImage;
},
disableUploadButton() {
return this.sourceType === UPLOAD && this.isUploading;
},
},
methods: {
done() {
if (this.uploadController) {
this.uploadController.abort();
}
this.$router.push({
name: this.doneRoute,
params: { resource: HCI.SETTING, product: 'harvester' }
});
},
async initImageValue() {
this.imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
metadata: {
name: '',
namespace: 'harvester-system',
generateName: 'image-',
annotations: {}
},
spec: {
sourceType: UPLOAD,
displayName: '',
checksum: ''
},
});
},
async save(buttonCb) {
let res = null;
this.file = {};
this.errors = [];
if (!this.imageValue.spec.displayName && this.uploadImage) {
if (!this.imageValue.spec.displayName && this.createNewImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false);
@@ -125,26 +215,11 @@ export default {
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD) {
this.imageValue.spec.sourceType = UPLOAD;
const file = this.file;
if (!file.name) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.selectExitImage'));
buttonCb(false);
return;
}
this.imageValue.spec.url = '';
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file.name;
res = await this.imageValue.save();
res.uploadImage(file);
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) {
this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
@@ -153,12 +228,12 @@ export default {
}
res = await this.imageValue.save();
this.value.spec.image = res.id;
}
this.value.spec.image = res.id;
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false);
return;
}
@@ -169,18 +244,87 @@ export default {
if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging;
}
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
}
await this.value.save();
this.done();
buttonCb(true);
} catch (e) {
this.errors = exceptionToErrorsArray(e);
this.errors = [e?.message] || exceptionToErrorsArray(e);
buttonCb(false);
}
},
handleFileUpload() {
this.file = this.$refs.file.files[0];
async uploadFile(file) {
const fileName = file.name;
if (!fileName) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
return;
}
const isDuplicatedFile = this.allOSImages.some((I) => I.spec.displayName === fileName);
if (isDuplicatedFile) {
this.errors.push(this.$store.getters['i18n/t']('harvester.upgradePage.upload.duplicatedFile'));
this.file = {};
return;
}
this.errors = [];
this.imageValue.spec.sourceType = UPLOAD;
this.imageValue.spec.displayName = fileName;
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = fileName;
this.imageValue.spec.url = '';
try {
const res = await this.imageValue.save();
this.uploadImageId = res.id;
this.uploadController = new AbortController();
const signal = this.uploadController.signal;
this.uploadResult = await res.uploadImage(file, { signal });
} catch (e) {
if (e?.code === 'ERR_NETWORK') {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.networkError'));
} else if (e?.code === 'ERR_CANCELED') {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.cancelUpload'));
} else {
this.errors = [e?.message] || exceptionToErrorsArray(e);
}
this.file = {};
this.uploadImageId = '';
}
},
handleImageDelete(imageId) {
const image = this.allOSImages.find((I) => I.id === imageId);
if (image) {
this.$store.dispatch('harvester/promptModal', {
resources: [image],
component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
});
this.deleteImageId = '';
}
},
async handleFileUpload() {
this.uploadImageId = '';
this.errors = [];
this.file = this.$refs.file?.files[0];
if (this.file) {
await this.initImageValue();
await this.uploadFile(this.file);
}
},
selectFile() {
@@ -190,21 +334,27 @@ export default {
},
watch: {
imageSource(neu) {
if (neu !== IMAGE_METHOD.DELETE) {
this.deleteImageId = '';
}
},
'imageValue.spec.url': {
handler(neu) {
const suffixName = neu?.split('/')?.pop();
const splitName = suffixName?.split('.') || [];
const fileSuffix = splitName?.pop()?.toLowerCase();
if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) {
if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) {
this.imageValue.spec.displayName = suffixName;
}
},
deep: true
},
file(neu) {
if (!this.imageValue.spec.displayName && neu.name) {
// update name input if select new image
if (neu.name && neu.name !== this.imageValue.spec.displayName) {
this.imageValue.spec.displayName = neu.name;
}
}
@@ -227,9 +377,11 @@ export default {
:errors="errors"
:can-yaml="false"
finish-button-mode="upgrade"
:validation-passed="enableUpgrade"
:cancel-event="true"
@finish="save"
@cancel="done"
@error="e=>errors = e"
>
<RadioGroup
v-model:value="imageSource"
@@ -238,18 +390,57 @@ export default {
:options="[
IMAGE_METHOD.NEW,
IMAGE_METHOD.EXIST,
IMAGE_METHOD.DELETE,
]"
:labels="[
t('harvester.upgradePage.uploadNew'),
t('harvester.upgradePage.selectExisting'),
t('harvester.upgradePage.deleteExisting'),
]"
/>
<UpgradeInfo />
<UpgradeInfo v-if="createNewImage || selectExistImage" />
<div v-if="uploadImage">
<Banner
v-if="showUploadSuccessBanner"
color="success"
class="mt-0 mb-30"
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
/>
<Banner
v-if="showUploadingWarningBanner"
color="warning"
class="mt-0 mb-30"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
/>
<div
v-if="showUpgradeOptions"
class="mt-10 mb-10"
>
<Checkbox
v-if="canEnableLogging"
v-model:value="enableLogging"
class="check mb-20"
type="checkbox"
:label="t('harvester.upgradePage.enableLogging')"
/>
<div
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
class="mb-20"
>
<Checkbox
v-model:value="skipSingleReplicaDetachedVol"
class="check"
type="checkbox"
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
/>
</div>
</div>
<div v-if="createNewImage">
<LabeledInput
v-model.trim="imageValue.spec.displayName"
v-model:value.trim="imageValue.spec.displayName"
class="mb-20"
label-key="harvester.fields.name"
required
@@ -261,14 +452,6 @@ export default {
label-key="harvester.setting.upgrade.checksum"
/>
<Checkbox
v-if="canEnableLogging"
v-model:value="enableLogging"
class="check mb-20"
type="checkbox"
:label="t('harvester.upgradePage.enableLogging')"
/>
<RadioGroup
v-model:value="sourceType"
class="mb-20 image-group"
@@ -285,7 +468,7 @@ export default {
<LabeledInput
v-if="sourceType === 'download'"
v-model.trim="imageValue.spec.url"
v-model:value.trim="imageValue.spec.url"
class="labeled-input--tooltip"
required
label-key="harvester.image.url"
@@ -298,6 +481,7 @@ export default {
<button
type="button"
class="btn role-primary"
:disabled="disableUploadButton"
@click="selectFile"
>
{{ t('harvester.image.uploadFile') }}
@@ -318,28 +502,56 @@ export default {
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
</span>
</div>
<ImagePercentageBar
v-if="showProgressBar"
class="mt-20"
:value="uploadProgress"
/>
</div>
<LabeledSelect
v-else
v-if="selectExistImage"
v-model:value="imageId"
:options="osImageOptions"
required
class="mb-20"
label-key="harvester.fields.image"
/>
<div
v-if="deleteExistImage"
class="mt-20"
>
<Banner
color="info"
class="mt-10 mb-30"
:label="t('harvester.upgradePage.deleteHeader')"
/>
<LabeledSelect
v-model:value="deleteImageId"
:options="deleteOSImageOptions"
required
class="mb-20"
label-key="harvester.fields.image"
@update:value="handleImageDelete"
/>
</div>
</CruResource>
</div>
</template>
<style lang="scss" scoped>
#air-gap {
padding: 20px;
:deep() .image-group .radio-group {
display: flex;
.radio-container {
margin-right: 30px;
}
}
.parent {
grid-template-columns:auto 40px;
}
.chooseFile {
display: flex;
align-items: center;
+135 -1027
View File
File diff suppressed because it is too large Load Diff