diff --git a/pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue b/pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue
index 36bd0c3e..ec33bbce 100644
--- a/pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue
+++ b/pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue
@@ -32,7 +32,7 @@ export default {
resources: {
type: Array,
required: true
- }
+ },
},
data() {
@@ -43,7 +43,7 @@ export default {
...mapState('action-menu', ['modalData']),
title() {
- return this.modalData.title || 'dialog.promptRemove.title';
+ return this.modalData?.title || 'dialog.promptRemove.title';
},
formattedType() {
@@ -51,7 +51,7 @@ export default {
},
warningMessage() {
- if (this.modalData.warningMessage) return this.modalData.warningMessage;
+ if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
@@ -145,6 +145,7 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
+ if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
diff --git a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue
index 4cfb2c33..b6cfb791 100644
--- a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue
+++ b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue
@@ -12,6 +12,7 @@ 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';
+import { STORAGE_CLASS } from '@shell/config/types';
const IMAGE_METHOD = {
NEW: 'new',
@@ -32,6 +33,7 @@ export default {
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
+ await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
const value = await this.$store.dispatch('harvester/create', {
type: HCI.UPGRADE,
@@ -63,6 +65,7 @@ export default {
sourceType: UPLOAD,
uploadController: null,
uploadResult: null,
+ storageClassValue: null,
imageValue: null,
enableLogging: true,
IMAGE_METHOD,
@@ -79,7 +82,6 @@ export default {
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
-
allOSImages() {
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
},
@@ -116,7 +118,7 @@ export default {
},
fileName() {
- return this.file?.name || '';
+ return this.preprocessImageName(this.file?.name || '');
},
canEnableLogging() {
@@ -181,6 +183,38 @@ export default {
});
},
+ async createImageStorageClass(imageName = '') {
+ // delete related SC if existed
+ await this.deleteImageStorageClass(imageName);
+
+ const storageClassPayload = {
+ apiVersion: 'storage.k8s.io/v1',
+ type: STORAGE_CLASS,
+ metadata: { name: imageName },
+ volumeBindingMode: 'Immediate',
+ reclaimPolicy: 'Delete',
+ allowVolumeExpansion: true, // must be boolean type
+ provisioner: 'driver.longhorn.io',
+ };
+
+ this.storageClassValue = await this.$store.dispatch('harvester/create', storageClassPayload);
+
+ if (this.storageClassValue && this.storageClassValue.save) {
+ await this.storageClassValue.save();
+ }
+ },
+
+ async deleteImageStorageClass(imageName = '') {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+ const storageClasses = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
+
+ const targetSC = storageClasses.find((sc) => sc.id === imageName);
+
+ if (targetSC && targetSC.remove) {
+ await targetSC.remove();
+ }
+ },
+
async initImageValue() {
this.imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
@@ -191,6 +225,7 @@ export default {
annotations: {}
},
spec: {
+ backend: 'cdi',
sourceType: UPLOAD,
displayName: '',
checksum: this.imageValue?.spec?.checksum || '',
@@ -203,8 +238,9 @@ export default {
this.file = {};
this.errors = [];
+ const imageDisplayName = this.imageValue?.spec?.displayName || '';
- if (!this.imageValue.spec.displayName && this.createNewImage) {
+ if (!imageDisplayName && this.createNewImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false);
@@ -212,24 +248,31 @@ export default {
}
try {
+ // Save the image first if creating a new one
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
- this.imageValue.spec.sourceType = DOWNLOAD;
+ // check if URL is provided
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
return;
}
+
+ // create related image storage class first
+ await this.createImageStorageClass(imageDisplayName);
+ this.imageValue.spec.sourceType = DOWNLOAD;
+ this.imageValue.spec.targetStorageClassName = imageDisplayName;
+
res = await this.imageValue.save();
this.value.spec.image = res.id;
}
- } else if (this.imageSource === IMAGE_METHOD.EXIST) {
+ } else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false);
@@ -239,7 +282,7 @@ export default {
this.value.spec.image = this.imageId;
}
-
+ // enable logging or skip single replica detection if checked
if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging;
}
@@ -252,11 +295,13 @@ export default {
} catch (e) {
this.errors = [e?.message] || exceptionToErrorsArray(e);
buttonCb(false);
+ // if anything failed, delete the created image storage class
+ await this.deleteImageStorageClass(imageDisplayName);
}
},
async uploadFile(file) {
- const fileName = file.name;
+ const fileName = this.preprocessImageName(file.name);
if (!fileName) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
@@ -280,6 +325,10 @@ export default {
this.imageValue.spec.url = '';
try {
+ // before uploading image, we need to create related image storage class first
+ await this.createImageStorageClass(fileName);
+ this.imageValue.spec.targetStorageClassName = fileName;
+
const res = await this.imageValue.save();
this.uploadImageId = res.id;
@@ -296,20 +345,35 @@ export default {
} else {
this.errors = exceptionToErrorsArray(e);
}
+ // if upload failed, delete the created image storage class
+ await this.deleteImageStorageClass(fileName);
this.file = {};
this.uploadImageId = '';
}
},
+ // replace _ to - to meet storage class name requirement
+ preprocessImageName(name) {
+ if (!name) {
+ return '';
+ }
+
+ return name.toLowerCase().replace(/[_]/g, '-');
+ },
+
handleImageDelete(imageId) {
const image = this.allOSImages.find((I) => I.id === imageId);
+ const imageDisplayName = image?.spec?.displayName || '';
- if (image) {
+ if (image && imageDisplayName) {
this.$store.dispatch('harvester/promptModal', {
- resources: [image],
- component: 'ConfirmRelatedToRemoveDialog',
- needConfirmation: false,
- warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
+ resources: [image],
+ component: 'ConfirmRelatedToRemoveDialog',
+ needConfirmation: false,
+ warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
+ extraActionAfterRemove: async() => {
+ await this.deleteImageStorageClass(imageDisplayName);
+ }
});
this.deleteImageId = '';
}
@@ -419,13 +483,13 @@ export default {
v-if="showUploadSuccessBanner"
color="success"
class="mt-0 mb-30"
- :label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
+ :label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
/>