Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restricted permissions for storage pool [WD-19339] #1111

Merged
merged 12 commits into from
Feb 21, 2025
Merged
8 changes: 4 additions & 4 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export const fetchNetworks = (
target?: string,
): Promise<LxdNetwork[]> => {
const targetParam = target ? `&target=${target}` : "";
const entitlements = `&${withEntitlementsQuery(
const entitlements = withEntitlementsQuery(
isFineGrained,
networkEntitlements,
)}`;
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/networks?project=${project}&recursion=1${targetParam}${entitlements}`,
Expand Down Expand Up @@ -81,10 +81,10 @@ export const fetchNetwork = (
target?: string,
): Promise<LxdNetwork> => {
const targetParam = target ? `&target=${target}` : "";
const entitlements = `&${withEntitlementsQuery(
const entitlements = withEntitlementsQuery(
isFineGrained,
networkEntitlements,
)}`;
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/networks/${name}?project=${project}${targetParam}${entitlements}`,
Expand Down
47 changes: 40 additions & 7 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,38 @@ import type { LxdOperationResponse } from "types/operation";
import axios, { AxiosResponse } from "axios";
import type { LxdClusterMember } from "types/cluster";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";
import { withEntitlementsQuery } from "util/entitlements/api";

export const storagePoolEntitlements = ["can_edit", "can_delete"];
export const storageVolumeEntitlements = ["can_delete"];

export const fetchStoragePool = (
pool: string,
isFineGrained: boolean | null,
target?: string,
): Promise<LxdStoragePool> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storagePoolEntitlements,
);
return new Promise((resolve, reject) => {
const targetParam = target ? `&target=${target}` : "";
fetch(`/1.0/storage-pools/${pool}?recursion=1${targetParam}`)
fetch(`/1.0/storage-pools/${pool}?recursion=1${targetParam}${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStoragePool>) => resolve(data.metadata))
.catch(reject);
});
};

export const fetchStoragePools = (): Promise<LxdStoragePool[]> => {
export const fetchStoragePools = (
isFineGrained: boolean | null,
): Promise<LxdStoragePool[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storagePoolEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools?recursion=1`)
fetch(`/1.0/storage-pools?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStoragePool[]>) => resolve(data.metadata))
.catch(reject);
Expand Down Expand Up @@ -204,11 +219,12 @@ export const deleteStoragePool = (pool: string): Promise<void> => {
export const fetchPoolFromClusterMembers = (
poolName: string,
clusterMembers: LxdClusterMember[],
isFineGrained: boolean | null,
): Promise<LXDStoragePoolOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchStoragePool(poolName, member.server_name);
return fetchStoragePool(poolName, isFineGrained, member.server_name);
}),
)
.then((results) => {
Expand All @@ -235,9 +251,16 @@ export const fetchPoolFromClusterMembers = (
export const fetchStorageVolumes = (
pool: string,
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${pool}/volumes?project=${project}&recursion=1`)
fetch(
`/1.0/storage-pools/${pool}/volumes?project=${project}&recursion=1${entitlements}`,
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) =>
resolve(data.metadata.map((volume) => ({ ...volume, pool }))),
Expand All @@ -248,9 +271,14 @@ export const fetchStorageVolumes = (

export const fetchAllStorageVolumes = (
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-volumes?recursion=1&project=${project}`)
fetch(`/1.0/storage-volumes?recursion=1&project=${project}${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) =>
resolve(data.metadata),
Expand All @@ -264,10 +292,15 @@ export const fetchStorageVolume = (
project: string,
type: string,
volume: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}?project=${project}&recursion=1`,
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}?project=${project}&recursion=1${entitlements}`,
)
.then(handleEtagResponse)
.then((data) => resolve({ ...data, pool } as LxdStorageVolume))
Expand Down
10 changes: 8 additions & 2 deletions src/components/forms/ClusterSpecificInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface Props {
helpText?: string | ReactNode;
placeholder?: string;
classname?: string;
disabledReason?: string;
}

const ClusterSpecificInput: FC<Props> = ({
Expand All @@ -36,6 +37,7 @@ const ClusterSpecificInput: FC<Props> = ({
helpText,
placeholder,
classname = "u-sv3",
disabledReason,
}) => {
const [isSpecific, setIsSpecific] = useState<boolean | null>(
isDefaultSpecific,
Expand Down Expand Up @@ -80,6 +82,8 @@ const ClusterSpecificInput: FC<Props> = ({
}
setIsSpecific((val) => !val);
}}
disabled={!!disabledReason}
title={disabledReason}
/>
)}
{isSpecific && (
Expand Down Expand Up @@ -118,8 +122,9 @@ const ClusterSpecificInput: FC<Props> = ({
className="u-no-margin--bottom"
value={activeValue}
onChange={(e) => setValueForMember(e.target.value, item)}
disabled={disabled}
disabled={!!disabledReason || disabled}
placeholder={placeholder}
title={disabledReason}
/>
)}
</div>
Expand Down Expand Up @@ -151,9 +156,10 @@ const ClusterSpecificInput: FC<Props> = ({
type="text"
value={firstValue}
onChange={(e) => setValueForAllMembers(e.target.value)}
disabled={disabled}
disabled={!!disabledReason || disabled}
help={helpText}
placeholder={placeholder}
title={disabledReason}
/>
)}
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/components/forms/ClusteredDiskSizeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface Props {
setValue: (value: ClusterSpecificValues) => void;
values?: ClusterSpecificValues;
helpText?: string;
disabledReason?: string;
}

const ClusteredDiskSizeSelector: FC<Props> = ({
id,
setValue,
values,
helpText,
disabledReason,
}) => {
const { data: clusterMembers = [] } = useClusterMembers();
const memberNames = clusterMembers.map((member) => member.server_name);
Expand Down Expand Up @@ -59,6 +61,8 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
setValueForAllMembers(firstValue);
setIsSpecific((val) => !val);
}}
disabled={!!disabledReason}
title={disabledReason}
/>
}
{isSpecific && (
Expand All @@ -81,7 +85,7 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
id={memberNames.indexOf(item) === 0 ? id : `${id}-${item}`}
value={activeValue}
setMemoryLimit={(value) => setValueForMember(value, item)}
disabled={false}
disabled={!!disabledReason}
classname="u-no-margin--bottom"
/>
</div>
Expand All @@ -103,7 +107,7 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
id={id}
value={firstValue}
setMemoryLimit={(value) => setValueForAllMembers(value)}
disabled={false}
disabled={!!disabledReason}
help={helpText}
/>
}
Expand Down
9 changes: 2 additions & 7 deletions src/components/forms/DiskDeviceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { FC } from "react";
import { Input, useNotify } from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchStoragePools } from "api/storage-pools";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import Loader from "components/Loader";
import { getInheritedDiskDevices } from "util/configInheritance";
Expand All @@ -12,6 +9,7 @@ import DiskDeviceFormCustom from "./DiskDeviceFormCustom";
import classnames from "classnames";
import ScrollableForm from "components/ScrollableForm";
import { useProfiles } from "context/useProfiles";
import { useStoragePools } from "context/useStoragePools";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -35,10 +33,7 @@ const DiskDeviceForm: FC<Props> = ({ formik, project }) => {
data: pools = [],
isLoading: isStorageLoading,
error: storageError,
} = useQuery({
queryKey: [queryKeys.storage],
queryFn: () => fetchStoragePools(),
});
} = useStoragePools();

if (storageError) {
notify.failure("Loading storage pools failed", storageError);
Expand Down
4 changes: 4 additions & 0 deletions src/components/forms/DiskSizeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Props {
setMemoryLimit: (val: string) => void;
disabled?: boolean;
classname?: string;
disabledReason?: string;
}

const DiskSizeSelector: FC<Props> = ({
Expand All @@ -23,6 +24,7 @@ const DiskSizeSelector: FC<Props> = ({
setMemoryLimit,
disabled,
classname,
disabledReason,
}) => {
const limit = parseMemoryLimit(value) ?? {
value: 1,
Expand Down Expand Up @@ -55,6 +57,7 @@ const DiskSizeSelector: FC<Props> = ({
value={value?.match(/^\d/) ? limit.value : ""}
disabled={disabled}
className={classname}
title={disabledReason}
/>
<Select
id={`memUnitSelect-${id}`}
Expand All @@ -68,6 +71,7 @@ const DiskSizeSelector: FC<Props> = ({
value={limit.unit}
disabled={disabled}
className={classname}
title={disabledReason}
/>
</div>
{(help || helpTotal) && (
Expand Down
7 changes: 6 additions & 1 deletion src/context/loadCustomVolumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { loadVolumes } from "context/loadIsoVolumes";
export const loadCustomVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const result: LxdStorageVolume[] = [];

const volumes = await loadVolumes(project, hasStorageVolumesAll);
const volumes = await loadVolumes(
project,
hasStorageVolumesAll,
isFineGrained,
);
volumes.forEach((volume) => {
const contentTypes = ["filesystem", "block"];
const isFilesystemOrBlock = contentTypes.includes(volume.content_type);
Expand Down
19 changes: 14 additions & 5 deletions src/context/loadIsoVolumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import type { LxdStorageVolume } from "types/storage";
export const loadIsoVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<RemoteImage[]> => {
const remoteImages: RemoteImage[] = [];
const allVolumes = await loadVolumes(project, hasStorageVolumesAll);
const allVolumes = await loadVolumes(
project,
hasStorageVolumesAll,
isFineGrained,
);
allVolumes.forEach((volume) => {
if (volume.content_type === "iso") {
const image = isoToRemoteImage(volume);
Expand All @@ -26,20 +31,24 @@ export const loadIsoVolumes = async (
export const loadVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
return hasStorageVolumesAll
? fetchAllStorageVolumes(project)
: collectAllStorageVolumes(project);
? fetchAllStorageVolumes(project, isFineGrained)
: collectAllStorageVolumes(project, isFineGrained);
};

export const collectAllStorageVolumes = async (
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const allVolumes: LxdStorageVolume[] = [];
const pools = await fetchStoragePools();
const pools = await fetchStoragePools(isFineGrained);

const poolVolumes = await Promise.allSettled(
pools.map(async (pool) => fetchStorageVolumes(pool.name, project)),
pools.map(async (pool) =>
fetchStorageVolumes(pool.name, project, isFineGrained),
),
);

poolVolumes.forEach((result, index) => {
Expand Down
48 changes: 48 additions & 0 deletions src/context/useStoragePools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { UseQueryResult } from "@tanstack/react-query";
import { useAuth } from "./auth";
import { LxdStoragePool, LXDStoragePoolOnClusterMember } from "types/storage";
import {
fetchPoolFromClusterMembers,
fetchStoragePool,
fetchStoragePools,
} from "api/storage-pools";
import { useClusterMembers } from "./useClusterMembers";

export const useStoragePool = (
pool: string,
target?: string,
enabled?: boolean,
): UseQueryResult<LxdStoragePool> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.storage, pool, target],
queryFn: () => fetchStoragePool(pool, isFineGrained, target),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};

export const useStoragePools = (
enabled?: boolean,
): UseQueryResult<LxdStoragePool[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.storage],
queryFn: () => fetchStoragePools(isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};

export const usePoolFromClusterMembers = (
pool: string,
): UseQueryResult<LXDStoragePoolOnClusterMember[]> => {
const { isFineGrained } = useAuth();
const { data: clusterMembers = [] } = useClusterMembers();
return useQuery({
queryKey: [queryKeys.storage, pool, queryKeys.cluster],
queryFn: () =>
fetchPoolFromClusterMembers(pool, clusterMembers, isFineGrained),
enabled: isFineGrained !== null && clusterMembers.length > 0,
});
};
Loading