From ae9d3e8236931bc73ed1a0f682518c7d841593e5 Mon Sep 17 00:00:00 2001 From: David Edler Date: Wed, 16 Oct 2024 19:25:26 +0200 Subject: [PATCH] fix(instance) prevent local volume attach to instances on another cluster member Signed-off-by: David Edler --- src/api/storage-pools.tsx | 14 +++++-- src/components/forms/DiskDeviceFormCustom.tsx | 7 +++- src/pages/instances/CreateInstance.tsx | 1 + src/pages/instances/EditInstance.tsx | 1 + .../forms/InstanceCreateDetailsForm.tsx | 1 + src/pages/storage/CustomVolumeCreateModal.tsx | 40 ++++++++++++++++--- src/pages/storage/CustomVolumeModal.tsx | 14 ++++++- src/pages/storage/CustomVolumeSelectBtn.tsx | 4 ++ src/pages/storage/CustomVolumeSelectModal.tsx | 39 ++++++++++++++++-- .../storage/forms/StorageVolumeFormMain.tsx | 6 ++- src/util/instanceLocation.tsx | 27 +++++++++++++ tests/helpers/devices.ts | 2 +- 12 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 src/util/instanceLocation.tsx diff --git a/src/api/storage-pools.tsx b/src/api/storage-pools.tsx index 52ae4c6e3a..a397f35329 100644 --- a/src/api/storage-pools.tsx +++ b/src/api/storage-pools.tsx @@ -310,12 +310,18 @@ export const createStorageVolume = ( pool: string, project: string, volume: Partial, + target?: string, ): Promise => { + const targetParam = target ? `&target=${target}` : ""; + return new Promise((resolve, reject) => { - fetch(`/1.0/storage-pools/${pool}/volumes?project=${project}`, { - method: "POST", - body: JSON.stringify(volume), - }) + fetch( + `/1.0/storage-pools/${pool}/volumes?project=${project}${targetParam}`, + { + method: "POST", + body: JSON.stringify(volume), + }, + ) .then(handleResponse) .then(resolve) .catch(reject); diff --git a/src/components/forms/DiskDeviceFormCustom.tsx b/src/components/forms/DiskDeviceFormCustom.tsx index 8ff95fd39d..79b4867ddf 100644 --- a/src/components/forms/DiskDeviceFormCustom.tsx +++ b/src/components/forms/DiskDeviceFormCustom.tsx @@ -120,6 +120,7 @@ const DiskDeviceFormCustom: FC = ({ {!readOnly && ( changeVolume(volume, formVolume, index)} buttonProps={{ @@ -263,7 +264,11 @@ const DiskDeviceFormCustom: FC = ({ )} {!readOnly && ( - + Attach disk device diff --git a/src/pages/instances/CreateInstance.tsx b/src/pages/instances/CreateInstance.tsx index 99d9666328..ab821102bb 100644 --- a/src/pages/instances/CreateInstance.tsx +++ b/src/pages/instances/CreateInstance.tsx @@ -286,6 +286,7 @@ const CreateInstance: FC = () => { devices: [], readOnly: false, entityType: "instance", + isCreating: true, }, validationSchema: InstanceSchema, onSubmit: (values) => { diff --git a/src/pages/instances/EditInstance.tsx b/src/pages/instances/EditInstance.tsx index 61750aa08c..9ea14816fc 100644 --- a/src/pages/instances/EditInstance.tsx +++ b/src/pages/instances/EditInstance.tsx @@ -69,6 +69,7 @@ export interface InstanceEditDetailsFormValues { profiles: string[]; entityType: "instance"; readOnly: boolean; + isCreating: boolean; } export type EditInstanceFormValues = InstanceEditDetailsFormValues & diff --git a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx index 50362373f3..e708ac9721 100644 --- a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx +++ b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx @@ -34,6 +34,7 @@ export interface InstanceDetailsFormValues { target?: string; entityType: "instance"; readOnly: boolean; + isCreating: boolean; } export const instanceDetailPayload = (values: CreateInstanceFormValues) => { diff --git a/src/pages/storage/CustomVolumeCreateModal.tsx b/src/pages/storage/CustomVolumeCreateModal.tsx index c1490a2d9b..715da98af3 100644 --- a/src/pages/storage/CustomVolumeCreateModal.tsx +++ b/src/pages/storage/CustomVolumeCreateModal.tsx @@ -5,24 +5,27 @@ import { volumeFormToPayload, } from "pages/storage/forms/StorageVolumeForm"; import { useFormik } from "formik"; -import { createStorageVolume } from "api/storage-pools"; +import { createStorageVolume, fetchStoragePools } from "api/storage-pools"; import { queryKeys } from "util/queryKeys"; import * as Yup from "yup"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import StorageVolumeFormMain from "pages/storage/forms/StorageVolumeFormMain"; import { updateMaxHeight } from "util/updateMaxHeight"; import useEventListener from "@use-it/event-listener"; import { testDuplicateStorageVolumeName } from "util/storageVolume"; import { LxdStorageVolume } from "types/storage"; +import { useSettings } from "context/useSettings"; interface Props { project: string; + instanceLocation?: string; onCancel: () => void; onFinish: (volume: LxdStorageVolume) => void; } const CustomVolumeCreateModal: FC = ({ project, + instanceLocation, onCancel, onFinish, }) => { @@ -30,6 +33,13 @@ const CustomVolumeCreateModal: FC = ({ const queryClient = useQueryClient(); const controllerState = useState(null); + const { data: settings } = useSettings(); + + const { data: pools = [] } = useQuery({ + queryKey: [queryKeys.storage], + queryFn: () => fetchStoragePools(project), + }); + const StorageVolumeSchema = Yup.object().shape({ name: Yup.string() .test( @@ -38,6 +48,14 @@ const CustomVolumeCreateModal: FC = ({ .required("This field is required"), }); + const isLocalPool = (poolName: string) => { + const pool = pools.find((pool) => pool.name === poolName); + const driverDetails = settings?.environment?.storage_supported_drivers.find( + (driver) => driver.Name === pool?.driver, + ); + return !driverDetails?.Remote ?? false; + }; + const formik = useFormik({ initialValues: { content_type: "filesystem", @@ -53,7 +71,9 @@ const CustomVolumeCreateModal: FC = ({ validationSchema: StorageVolumeSchema, onSubmit: (values) => { const volume = volumeFormToPayload(values, project); - createStorageVolume(values.pool, project, volume) + const target = isLocalPool(values.pool) ? instanceLocation : undefined; + + createStorageVolume(values.pool, project, volume, target) .then(() => { void queryClient.invalidateQueries({ queryKey: [queryKeys.storage], @@ -71,6 +91,12 @@ const CustomVolumeCreateModal: FC = ({ }, }); + const validPool = + !isLocalPool(formik.values.pool) || instanceLocation !== "any"; + const poolError = validPool + ? undefined + : "Please select a remote storage pool, or set a cluster member for the instance"; + const updateFormHeight = () => { updateMaxHeight("volume-create-form", "p-modal__footer", 32, undefined, []); }; @@ -80,7 +106,11 @@ const CustomVolumeCreateModal: FC = ({ return ( <>
- +