diff --git a/pkg/harvester/config/harvester-map.js b/pkg/harvester/config/harvester-map.js
index 02cbfe25063..39e01ad49ac 100644
--- a/pkg/harvester/config/harvester-map.js
+++ b/pkg/harvester/config/harvester-map.js
@@ -69,3 +69,12 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
};
+
+export const CSI_SECRETS = {
+ CSI_PROVISIONER_SECRET_NAME: 'csi.storage.k8s.io/provisioner-secret-name',
+ CSI_PROVISIONER_SECRET_NAMESPACE: 'csi.storage.k8s.io/provisioner-secret-namespace',
+ CSI_NODE_PUBLISH_SECRET_NAME: 'csi.storage.k8s.io/node-publish-secret-name',
+ CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
+ 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',
+};
diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js
index fa0c42bfcb4..b580f4f0026 100644
--- a/pkg/harvester/config/table-headers.js
+++ b/pkg/harvester/config/table-headers.js
@@ -8,7 +8,6 @@ export const IMAGE_DOWNLOAD_SIZE = {
labelKey: 'tableHeaders.size',
value: 'downSize',
sort: 'status.size',
- width: 120
};
export const IMAGE_VIRTUAL_SIZE = {
@@ -16,7 +15,6 @@ export const IMAGE_VIRTUAL_SIZE = {
labelKey: 'harvester.tableHeaders.virtualSize',
value: 'virtualSize',
sort: 'status.virtualSize',
- width: 120
};
export const IMAGE_PROGRESS = {
diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue
index 98677d4e27c..0776d940ebe 100644
--- a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue
+++ b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue
@@ -7,8 +7,9 @@ import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { findBy } from '@shell/utils/array';
import { get } from '@shell/utils/object';
-
+import { ucFirst } from '@shell/utils/string';
import Storage from './Storage';
+import { SECRET } from '@shell/config/types';
export default {
components: {
@@ -26,8 +27,14 @@ export default {
},
},
+ async fetch() {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+
+ this.secrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
+ },
+
data() {
- return {};
+ return { secrets: [] };
},
computed: {
@@ -57,6 +64,21 @@ export default {
return this.value?.spec?.sourceType === 'upload';
},
+ encryptionSecret() {
+ if (!this.value.isEncrypted) {
+ return '-';
+ }
+
+ return this.value.encryptionSecret;
+ },
+ secretLink() {
+ return this.secrets.find(sc => sc.id === this.value.encryptionSecret)?.detailLocation;
+ },
+
+ isEncryptedString() {
+ return ucFirst(String(this.value.isEncrypted));
+ },
+
imageName() {
return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-';
},
@@ -116,6 +138,29 @@ export default {
+
diff --git a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
index 8846f9f87e8..2845918efbd 100644
--- a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
+++ b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
@@ -63,10 +63,7 @@ export default {
imageName() {
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
-
- const image = imageList.find( (I) => {
- return this.value.rootImageId === I.id;
- });
+ const image = imageList.find( I => this.value.rootImageId === I.id);
return image?.spec?.displayName || 'N/A';
},
diff --git a/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue b/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue
index 329f61d2bfc..aef2cc02961 100644
--- a/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue
+++ b/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue
@@ -3,21 +3,34 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup';
-
+import { SECRET, NAMESPACE, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW } from '@shell/config/query-params';
-import { LONGHORN } from '@shell/config/types';
+import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
+// UI components for Longhorn storage class parameters
const DEFAULT_PARAMETERS = [
'numberOfReplicas',
'staleReplicaTimeout',
'diskSelector',
'nodeSelector',
'migratable',
+ 'encrypted',
];
+const {
+ CSI_PROVISIONER_SECRET_NAME,
+ CSI_PROVISIONER_SECRET_NAMESPACE,
+ CSI_NODE_PUBLISH_SECRET_NAME,
+ CSI_NODE_PUBLISH_SECRET_NAMESPACE,
+ CSI_NODE_STAGE_SECRET_NAME,
+ CSI_NODE_STAGE_SECRET_NAMESPACE
+} = CSI_SECRETS;
+
export default {
+ name: 'DriverLonghornIO',
+
components: {
KeyValue,
LabeledSelect,
@@ -40,6 +53,16 @@ export default {
},
},
+ async fetch() {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+
+ await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
+
+ const allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
+
+ // only show non-system secret to user to select
+ this.secrets = allSecrets.filter(secret => secret.isSystem === false);
+ },
data() {
if (this.realMode === _CREATE) {
this.$set(this.value, 'parameters', {
@@ -47,13 +70,13 @@ export default {
staleReplicaTimeout: '30',
diskSelector: null,
nodeSelector: null,
+ encrypted: 'false',
migratable: 'true',
});
}
- return {};
+ return { secrets: [] };
},
-
computed: {
longhornNodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -97,11 +120,29 @@ export default {
}];
},
+ secretOptions() {
+ return this.secrets.map(secret => secret.id);
+ },
+
+ volumeEncryptionOptions() {
+ return [{
+ label: this.t('generic.yes'),
+ value: 'true'
+ }, {
+ label: this.t('generic.no'),
+ value: 'false'
+ }];
+ },
+
parameters: {
get() {
const parameters = clone(this.value?.parameters) || {};
- DEFAULT_PARAMETERS.map((key) => {
+ DEFAULT_PARAMETERS.forEach((key) => {
+ delete parameters[key];
+ });
+
+ Object.values(CSI_SECRETS).forEach((key) => {
delete parameters[key];
});
@@ -113,6 +154,46 @@ export default {
}
},
+ volumeEncryption: {
+ set(neu) {
+ this.$set(this.value, 'parameters', {
+ ...this.value.parameters,
+ encrypted: neu
+ });
+ },
+
+ get() {
+ return this.value?.parameters?.encrypted || 'false';
+ }
+ },
+
+ secret: {
+ get() {
+ const selectedNs = this.value.parameters[CSI_PROVISIONER_SECRET_NAMESPACE];
+ const selectedName = this.value.parameters[CSI_PROVISIONER_SECRET_NAME];
+
+ if (selectedNs && selectedName) {
+ return `${ selectedNs }/${ selectedName }`;
+ }
+
+ return '';
+ },
+
+ set(selectedSecret) {
+ const [namespace, name] = selectedSecret.split('/');
+
+ this.$set(this.value, 'parameters', {
+ ...this.value.parameters,
+ [CSI_PROVISIONER_SECRET_NAME]: name,
+ [CSI_NODE_PUBLISH_SECRET_NAME]: name,
+ [CSI_NODE_STAGE_SECRET_NAME]: name,
+ [CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
+ [CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
+ [CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
+ });
+ }
+ },
+
nodeSelector: {
get() {
const nodeSelector = this.value?.parameters?.nodeSelector;
@@ -221,14 +302,31 @@ export default {
-
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
index 88c5dda9c1a..7a3c8c5e2ad 100644
--- a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
+++ b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
@@ -8,7 +8,6 @@ import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import Select from '@shell/components/form/Select';
import LabeledSelect from '@shell/components/form/LabeledSelect';
-
import CreateEditView from '@shell/mixins/create-edit-view';
import { OS } from '../mixins/harvester-vm';
import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
@@ -18,6 +17,9 @@ import { allHash } from '@shell/utils/promise';
import { STORAGE_CLASS } from '@shell/config/types';
import { HCI } from '../types';
+const ENCRYPT = 'encrypt';
+const DECRYPT = 'decrypt';
+const CLONE = 'clone';
const DOWNLOAD = 'download';
const UPLOAD = 'upload';
const rawORqcow2 = 'raw_qcow2';
@@ -57,6 +59,8 @@ export default {
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
this.$set(this, 'storageClassName', this.storageClassName || defaultStorage?.metadata?.name || 'longhorn');
+ this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
+ this.selectedImage = this.images.find(i => i.name === this.value.name) || null;
},
data() {
@@ -69,12 +73,14 @@ export default {
}
return {
- url: this.value.spec.url,
- files: [],
- resource: '',
- headers: {},
- fileUrl: '',
- file: '',
+ selectedImage: null,
+ images: [],
+ url: this.value.spec.url,
+ files: [],
+ resource: '',
+ headers: {},
+ fileUrl: '',
+ file: '',
};
},
@@ -92,9 +98,16 @@ export default {
},
showEditAsYaml() {
- return this.value.spec.sourceType === DOWNLOAD;
+ return this.value.spec.sourceType === DOWNLOAD || this.value.spec.sourceType === CLONE;
+ },
+ radioGroupOptions() {
+ return [
+ DOWNLOAD,
+ UPLOAD,
+ ENCRYPT,
+ DECRYPT
+ ];
},
-
storageClassOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
@@ -120,9 +133,66 @@ export default {
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
}
},
+ sourceImageOptions() {
+ let options = [];
+
+ if (this.value.spec.sourceType !== CLONE) {
+ return options;
+ }
+ if (this.value.spec.securityParameters.cryptoOperation === ENCRYPT) {
+ options = this.images.filter(image => !image.isEncrypted);
+ } else {
+ options = this.images.filter(image => image.isEncrypted);
+ }
+
+ return options.map(image => image.spec.displayName);
+ },
+ sourceImageName: {
+ get() {
+ return this.selectedImage?.spec.displayName;
+ },
+ set(imageDisplayName) {
+ this.selectedImage = this.images.find(i => i.spec.displayName === imageDisplayName);
+ // sourceImageName should bring the name of the image
+ this.value.spec.securityParameters.sourceImageName = this.selectedImage?.metadata.name || '';
+ }
+ },
+ sourceType: {
+ get() {
+ if (this.value.spec.sourceType === CLONE) {
+ return this.value.spec.securityParameters.cryptoOperation;
+ } else {
+ return this.value.spec.sourceType;
+ }
+ },
+
+ set(neu) {
+ if (neu === DECRYPT || neu === ENCRYPT) {
+ this.value.spec.sourceType = CLONE;
+ this.$set(this.value.spec, 'securityParameters', {
+ cryptoOperation: neu,
+ sourceImageName: '',
+ sourceImageNamespace: this.value.metadata.namespace
+ });
+ this.selectedImage = null;
+ } else {
+ this.$delete(this.value.spec, 'securityParameters');
+ this.value.spec.sourceType = neu;
+ }
+ }
+ }
},
watch: {
+ 'value.metadata.namespace'(neu) {
+ if (this.value.spec.sourceType === CLONE) {
+ this.$set(this.value.spec, 'securityParameters', {
+ cryptoOperation: this.value.spec.securityParameters.cryptoOperation,
+ sourceImageName: '',
+ sourceImageNamespace: neu
+ });
+ }
+ },
'value.spec.url'(neu) {
const url = neu.trim();
@@ -297,15 +367,14 @@ export default {
>
@@ -331,7 +400,7 @@ export default {
:tooltip="t('harvester.image.urlTip', {}, true)"
/>
-
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue
index 4a992224ecc..a2565eacfdf 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue
@@ -111,7 +111,7 @@ export default {
c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
return {
@@ -270,7 +281,7 @@ export default {
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue
index 8cd29f05f15..58fe7a5a95a 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue
@@ -8,11 +8,14 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { formatSi, parseSi } from '@shell/utils/units';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
+import { _VIEW } from '@shell/config/query-params';
+import LabelValue from '@shell/components/LabelValue';
+import { ucFirst } from '@shell/utils/string';
export default {
name: 'HarvesterEditVolume',
components: {
- InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput,
+ InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
},
props: {
@@ -58,6 +61,13 @@ export default {
},
computed: {
+ encryptionValue() {
+ return ucFirst(String(this.value.isEncrypted));
+ },
+
+ isView() {
+ return this.mode === _VIEW;
+ },
pvcsResource() {
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
@@ -212,7 +222,7 @@ export default {
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index 8c8fc52c740..c639a01ab49 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -166,6 +166,7 @@ harvester:
reboot: Reboot
forceStop: Force Stop
tableHeaders:
+ imageEncryption: Encryption
size: Size
virtualSize: Virtual Size
progress: Progress
@@ -551,6 +552,10 @@ harvester:
addContainer: Add Container
setFirst: Set as root volume
saveVolume: Update Volume
+ encryption: Encryption
+ lockTooltip:
+ all: All volumes are encrypted.
+ partial: Some volumes are encrypted.
title:
vmImage: Image Volume
existingVolume: Existing Volume
@@ -723,6 +728,8 @@ harvester:
basics: Basics
url: URL
size: Size
+ isEncryption: Encryption
+ encryptionSecret: Encryption Secret
virtualSize: Virtual Size
urlTip: 'Supports the
raw
and
qcow2
image formats which are supported by
qemu. Bootable ISO images can also be used and are treated like
raw
images.'
fileName: File Name
@@ -731,6 +738,11 @@ harvester:
sourceType:
download: URL
upload: File
+ clone: Clone
+ encrypt: Encrypt
+ decrypt: Decrypt
+ sourceImage: Source Image
+ cryptoOperation: Crypto Operation
warning:
uploading: |-
{count, plural,
@@ -1056,6 +1068,8 @@ harvester:
storage:
label: Storage
useDefault: Use the default storage
+ volumeEncryption: Volume Encryption
+ secret: Secret
migratable:
label: Migratable
numberOfReplicas:
diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue
index 13dad01f333..0a49d1e7451 100644
--- a/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue
+++ b/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue
@@ -75,6 +75,22 @@ export default {
+
+
+
+
+ {{ row.nameDisplay }}
+
+
+
+ {{ row.nameDisplay }}
+
+
+ |
+
diff --git a/pkg/harvester/list/harvesterhci.io.volume.vue b/pkg/harvester/list/harvesterhci.io.volume.vue
index d261af9ee87..04f82a71213 100644
--- a/pkg/harvester/list/harvesterhci.io.volume.vue
+++ b/pkg/harvester/list/harvesterhci.io.volume.vue
@@ -158,6 +158,22 @@ export default {
+