-
Notifications
You must be signed in to change notification settings - Fork 60
fix(storage pool): rely on operation when api extension is present #1899
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||
| import { handleResponse } from "util/helpers"; | ||||||||
| import { handleResponse, LxdApiError } from "util/helpers"; | ||||||||
| import type { LxdOperation, LxdOperationList } from "types/operation"; | ||||||||
| import type { LxdApiResponse } from "types/apiResponse"; | ||||||||
| import { ROOT_PATH } from "util/rootPath"; | ||||||||
|
|
@@ -38,3 +38,48 @@ export const cancelOperation = async (id: string): Promise<void> => { | |||||||
| method: "DELETE", | ||||||||
| }).then(handleResponse); | ||||||||
| }; | ||||||||
|
|
||||||||
| export const waitForOperation = async ( | ||||||||
| id: string, | ||||||||
| member?: string, | ||||||||
| maxTimeoutMs = 120000, | ||||||||
| ): Promise<void> => { | ||||||||
| const endpoint = `${ROOT_PATH}/1.0/operations/${encodeURIComponent(id)}`; | ||||||||
| const startTime = Date.now(); | ||||||||
| let delay = 500; | ||||||||
|
|
||||||||
| while (true) { | ||||||||
| if (Date.now() - startTime > maxTimeoutMs) { | ||||||||
| const memberPrefix = member ? `[Member: ${member}] ` : ""; | ||||||||
| throw new Error(`${memberPrefix}Operation timed out.`); | ||||||||
| } | ||||||||
|
|
||||||||
| try { | ||||||||
| const response = (await fetch(endpoint).then( | ||||||||
| handleResponse, | ||||||||
| )) as LxdApiResponse<LxdOperation>; | ||||||||
|
Comment on lines
+42
to
+60
|
||||||||
| const operation = response.metadata; | ||||||||
|
|
||||||||
| if (operation.status_code === 200) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| if (operation.status_code >= 400) { | ||||||||
| throw new Error( | ||||||||
| operation.err || | ||||||||
| `Operation ${id} failed with status ${operation.status}`, | ||||||||
| ); | ||||||||
| } | ||||||||
| } catch (error) { | ||||||||
| if (error instanceof LxdApiError && error.status === 404) { | ||||||||
| const memberPrefix = member ? `[Member: ${member}] ` : ""; | ||||||||
| throw new Error(`${memberPrefix}Operation not found on server.`); | ||||||||
| } | ||||||||
|
||||||||
| } | |
| } | |
| throw error; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,11 @@ import type { | |
| import type { LxdApiResponse } from "types/apiResponse"; | ||
| import type { LxdClusterMember } from "types/cluster"; | ||
| import type { ClusterSpecificValues } from "types/cluster"; | ||
| import type { LxdOperationResponse } from "types/operation"; | ||
| import { addEntitlements } from "util/entitlements/api"; | ||
| import { addTarget } from "util/target"; | ||
| import { ROOT_PATH } from "util/rootPath"; | ||
| import { waitForOperation } from "api/operations"; | ||
|
|
||
| export const storagePoolEntitlements = ["can_edit", "can_delete"]; | ||
|
|
||
|
|
@@ -101,17 +103,21 @@ export const fetchClusteredStoragePoolResources = async ( | |
| export const createPool = async ( | ||
| pool: Partial<LxdStoragePool>, | ||
| target?: string, | ||
| ): Promise<void> => { | ||
| ): Promise<LxdOperationResponse> => { | ||
| const params = new URLSearchParams(); | ||
| addTarget(params, target); | ||
|
|
||
| await fetch(`${ROOT_PATH}/1.0/storage-pools?${params.toString()}`, { | ||
| return fetch(`${ROOT_PATH}/1.0/storage-pools?${params.toString()}`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(pool), | ||
| }).then(handleResponse); | ||
| }) | ||
| .then(handleResponse) | ||
| .then((data: LxdOperationResponse) => { | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| const getClusterAndMemberPoolPayload = (pool: LxdStoragePool) => { | ||
|
|
@@ -149,10 +155,11 @@ export const createClusteredPool = async ( | |
| sourcePerClusterMember?: ClusterSpecificValues, | ||
| zfsPoolNamePerClusterMember?: ClusterSpecificValues, | ||
| sizePerClusterMember?: ClusterSpecificValues, | ||
| ): Promise<void> => { | ||
| ): Promise<LxdOperationResponse> => { | ||
| const { memberPoolPayload, clusterPoolPayload } = | ||
| getClusterAndMemberPoolPayload(pool); | ||
| return Promise.allSettled( | ||
|
|
||
| const results = await Promise.allSettled( | ||
| clusterMembers.map(async (item) => { | ||
| const clusteredMemberPool = { | ||
| ...memberPoolPayload, | ||
|
|
@@ -163,23 +170,41 @@ export const createClusteredPool = async ( | |
| "zfs.pool_name": zfsPoolNamePerClusterMember?.[item.server_name], | ||
| }, | ||
| }; | ||
| return createPool(clusteredMemberPool, item.server_name); | ||
| const operation = await createPool(clusteredMemberPool, item.server_name); | ||
| return { operation, member: item.server_name }; | ||
| }), | ||
| ) | ||
| .then(handleSettledResult) | ||
| .then(async () => { | ||
| return createPool(clusterPoolPayload); | ||
| }); | ||
| ); | ||
|
|
||
| handleSettledResult(results); | ||
|
|
||
| const operationsWithMembers = results | ||
| .filter( | ||
| ( | ||
| res, | ||
| ): res is PromiseFulfilledResult<{ | ||
| operation: LxdOperationResponse; | ||
| member: string; | ||
| }> => res.status === "fulfilled", | ||
| ) | ||
| .map((res) => res.value); | ||
|
|
||
| await Promise.all( | ||
| operationsWithMembers.map(async ({ operation, member }) => { | ||
| await waitForOperation(operation.metadata.id, member); | ||
| }), | ||
| ); | ||
|
|
||
| return createPool(clusterPoolPayload); | ||
| }; | ||
|
|
||
| export const updatePool = async ( | ||
| pool: LxdStoragePool, | ||
| target?: string, | ||
| ): Promise<void> => { | ||
| ): Promise<LxdOperationResponse> => { | ||
| const params = new URLSearchParams(); | ||
| addTarget(params, target); | ||
|
|
||
| await fetch( | ||
| return fetch( | ||
| `${ROOT_PATH}/1.0/storage-pools/${encodeURIComponent(pool.name)}?${params.toString()}`, | ||
| { | ||
| method: "PATCH", | ||
|
|
@@ -188,7 +213,11 @@ export const updatePool = async ( | |
| }, | ||
| body: JSON.stringify(pool), | ||
| }, | ||
| ).then(handleResponse); | ||
| ) | ||
| .then(handleResponse) | ||
| .then((data: LxdOperationResponse) => { | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| export const updateClusteredPool = async ( | ||
|
|
@@ -197,10 +226,11 @@ export const updateClusteredPool = async ( | |
| sourcePerClusterMember?: ClusterSpecificValues, | ||
| zfsPoolNamePerClusterMember?: ClusterSpecificValues, | ||
| sizePerClusterMember?: ClusterSpecificValues, | ||
| ): Promise<void> => { | ||
| ): Promise<LxdOperationResponse> => { | ||
| const { memberPoolPayload, clusterPoolPayload } = | ||
| getClusterAndMemberPoolPayload(pool); | ||
| return Promise.allSettled( | ||
|
|
||
| const results = await Promise.allSettled( | ||
| clusterMembers.map(async (item) => { | ||
| const clusteredMemberPool = { | ||
| ...memberPoolPayload, | ||
|
|
@@ -220,32 +250,65 @@ export const updateClusteredPool = async ( | |
| clusteredMemberPool.config["zfs.pool_name"] = | ||
| zfsPoolNamePerClusterMember[item.server_name]; | ||
| } | ||
| return updatePool(clusteredMemberPool, item.server_name); | ||
| const operation = await updatePool(clusteredMemberPool, item.server_name); | ||
| return { operation, member: item.server_name }; | ||
| }), | ||
| ) | ||
| .then(handleSettledResult) | ||
| .then(async () => updatePool(clusterPoolPayload)); | ||
| ); | ||
|
|
||
| handleSettledResult(results); | ||
|
|
||
| const operationsWithMembers = results | ||
| .filter( | ||
| ( | ||
| res, | ||
| ): res is PromiseFulfilledResult<{ | ||
| operation: LxdOperationResponse; | ||
| member: string; | ||
| }> => res.status === "fulfilled", | ||
| ) | ||
| .map((res) => res.value); | ||
|
|
||
| await Promise.all( | ||
| operationsWithMembers.map(async ({ operation, member }) => { | ||
| await waitForOperation(operation.metadata.id, member); | ||
| }), | ||
| ); | ||
|
|
||
| return updatePool(clusterPoolPayload); | ||
| }; | ||
|
Comment on lines
223
to
278
|
||
|
|
||
| export const renameStoragePool = async ( | ||
| oldName: string, | ||
| newName: string, | ||
| ): Promise<void> => { | ||
| await fetch(`${ROOT_PATH}/1.0/storage-pools/${encodeURIComponent(oldName)}`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| ): Promise<LxdOperationResponse> => { | ||
| return fetch( | ||
| `${ROOT_PATH}/1.0/storage-pools/${encodeURIComponent(oldName)}`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| name: newName, | ||
| }), | ||
| }, | ||
| body: JSON.stringify({ | ||
| name: newName, | ||
| }), | ||
| }).then(handleResponse); | ||
| ) | ||
| .then(handleResponse) | ||
| .then((data: LxdOperationResponse) => { | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| export const deleteStoragePool = async (pool: string): Promise<void> => { | ||
| await fetch(`${ROOT_PATH}/1.0/storage-pools/${encodeURIComponent(pool)}`, { | ||
| export const deleteStoragePool = async ( | ||
| pool: string, | ||
| ): Promise<LxdOperationResponse> => { | ||
| return fetch(`${ROOT_PATH}/1.0/storage-pools/${encodeURIComponent(pool)}`, { | ||
| method: "DELETE", | ||
| }).then(handleResponse); | ||
| }) | ||
| .then(handleResponse) | ||
| .then((data: LxdOperationResponse) => { | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| export const fetchPoolFromClusterMembers = async ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
waitForOperation()accepts amemberargument but it isn’t used to target the correct cluster member when polling. For operations created via requests with?target=<member>, polling without the sametargetis likely to 404 or observe the wrong operation. Build the endpoint withURLSearchParams+addTarget(params, member)(or accept atargetparam) and include it in the fetch URL.