mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
feat: create related image storageclass before OS upgrade (#595)
* feat: create related image SC before upgrade Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: update spec.targetStorageClassName Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: based on comment Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
87e44cb658
commit
10d19cd329
@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -43,7 +43,7 @@ export default {
|
|||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return this.modalData.title || 'dialog.promptRemove.title';
|
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||||
},
|
},
|
||||||
|
|
||||||
formattedType() {
|
formattedType() {
|
||||||
@ -51,7 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
warningMessage() {
|
warningMessage() {
|
||||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||||
|
|
||||||
const isPlural = this.type.endsWith('s');
|
const isPlural = this.type.endsWith('s');
|
||||||
const thisOrThese = isPlural ? 'these' : 'this';
|
const thisOrThese = isPlural ? 'these' : 'this';
|
||||||
@ -145,6 +145,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
|
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester'
|
|||||||
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
|
||||||
const IMAGE_METHOD = {
|
const IMAGE_METHOD = {
|
||||||
NEW: 'new',
|
NEW: 'new',
|
||||||
@ -32,6 +33,7 @@ export default {
|
|||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
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', {
|
const value = await this.$store.dispatch('harvester/create', {
|
||||||
type: HCI.UPGRADE,
|
type: HCI.UPGRADE,
|
||||||
@ -63,6 +65,7 @@ export default {
|
|||||||
sourceType: UPLOAD,
|
sourceType: UPLOAD,
|
||||||
uploadController: null,
|
uploadController: null,
|
||||||
uploadResult: null,
|
uploadResult: null,
|
||||||
|
storageClassValue: null,
|
||||||
imageValue: null,
|
imageValue: null,
|
||||||
enableLogging: true,
|
enableLogging: true,
|
||||||
IMAGE_METHOD,
|
IMAGE_METHOD,
|
||||||
@ -79,7 +82,6 @@ export default {
|
|||||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||||
},
|
},
|
||||||
|
|
||||||
allOSImages() {
|
allOSImages() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
||||||
},
|
},
|
||||||
@ -116,7 +118,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fileName() {
|
fileName() {
|
||||||
return this.file?.name || '';
|
return this.preprocessImageName(this.file?.name || '');
|
||||||
},
|
},
|
||||||
|
|
||||||
canEnableLogging() {
|
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() {
|
async initImageValue() {
|
||||||
this.imageValue = await this.$store.dispatch('harvester/create', {
|
this.imageValue = await this.$store.dispatch('harvester/create', {
|
||||||
type: HCI.IMAGE,
|
type: HCI.IMAGE,
|
||||||
@ -191,6 +225,7 @@ export default {
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
backend: 'cdi',
|
||||||
sourceType: UPLOAD,
|
sourceType: UPLOAD,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
checksum: this.imageValue?.spec?.checksum || '',
|
checksum: this.imageValue?.spec?.checksum || '',
|
||||||
@ -203,8 +238,9 @@ export default {
|
|||||||
|
|
||||||
this.file = {};
|
this.file = {};
|
||||||
this.errors = [];
|
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') }));
|
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
@ -212,24 +248,31 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Save the image first if creating a new one
|
||||||
if (this.imageSource === IMAGE_METHOD.NEW) {
|
if (this.imageSource === IMAGE_METHOD.NEW) {
|
||||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||||
|
|
||||||
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
||||||
this.value.spec.image = this.uploadImageId;
|
this.value.spec.image = this.uploadImageId;
|
||||||
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
|
} 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) {
|
if (!this.imageValue.spec.url) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
return;
|
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();
|
res = await this.imageValue.save();
|
||||||
|
|
||||||
this.value.spec.image = res.id;
|
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) {
|
if (!this.imageId) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
@ -239,7 +282,7 @@ export default {
|
|||||||
|
|
||||||
this.value.spec.image = this.imageId;
|
this.value.spec.image = this.imageId;
|
||||||
}
|
}
|
||||||
|
// enable logging or skip single replica detection if checked
|
||||||
if (this.canEnableLogging) {
|
if (this.canEnableLogging) {
|
||||||
this.value.spec.logEnabled = this.enableLogging;
|
this.value.spec.logEnabled = this.enableLogging;
|
||||||
}
|
}
|
||||||
@ -252,11 +295,13 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
// if anything failed, delete the created image storage class
|
||||||
|
await this.deleteImageStorageClass(imageDisplayName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async uploadFile(file) {
|
async uploadFile(file) {
|
||||||
const fileName = file.name;
|
const fileName = this.preprocessImageName(file.name);
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
||||||
@ -280,6 +325,10 @@ export default {
|
|||||||
this.imageValue.spec.url = '';
|
this.imageValue.spec.url = '';
|
||||||
|
|
||||||
try {
|
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();
|
const res = await this.imageValue.save();
|
||||||
|
|
||||||
this.uploadImageId = res.id;
|
this.uploadImageId = res.id;
|
||||||
@ -296,20 +345,35 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.errors = exceptionToErrorsArray(e);
|
this.errors = exceptionToErrorsArray(e);
|
||||||
}
|
}
|
||||||
|
// if upload failed, delete the created image storage class
|
||||||
|
await this.deleteImageStorageClass(fileName);
|
||||||
this.file = {};
|
this.file = {};
|
||||||
this.uploadImageId = '';
|
this.uploadImageId = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// replace _ to - to meet storage class name requirement
|
||||||
|
preprocessImageName(name) {
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.toLowerCase().replace(/[_]/g, '-');
|
||||||
|
},
|
||||||
|
|
||||||
handleImageDelete(imageId) {
|
handleImageDelete(imageId) {
|
||||||
const image = this.allOSImages.find((I) => I.id === 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', {
|
this.$store.dispatch('harvester/promptModal', {
|
||||||
resources: [image],
|
resources: [image],
|
||||||
component: 'ConfirmRelatedToRemoveDialog',
|
component: 'ConfirmRelatedToRemoveDialog',
|
||||||
needConfirmation: false,
|
needConfirmation: false,
|
||||||
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
|
||||||
|
extraActionAfterRemove: async() => {
|
||||||
|
await this.deleteImageStorageClass(imageDisplayName);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.deleteImageId = '';
|
this.deleteImageId = '';
|
||||||
}
|
}
|
||||||
@ -419,13 +483,13 @@ export default {
|
|||||||
v-if="showUploadSuccessBanner"
|
v-if="showUploadSuccessBanner"
|
||||||
color="success"
|
color="success"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
:label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="showUploadingWarningBanner"
|
v-if="showUploadingWarningBanner"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
:label="t('harvester.image.warning.osUpgrade.uploading', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user