Skip to content

Commit

Permalink
fix(network) ensure network configuration is fully supported in a clu…
Browse files Browse the repository at this point in the history
…stered backend

Signed-off-by: David Edler <[email protected]>
  • Loading branch information
edlerd committed Jan 17, 2025
1 parent 3661fbe commit b52956e
Show file tree
Hide file tree
Showing 28 changed files with 1,207 additions and 257 deletions.
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ const App: FC = () => {
/>
}
/>
<Route
path="/ui/project/:project/member/:member/network/:name"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab"
element={
Expand Down
171 changes: 148 additions & 23 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import type { LxdNetwork, LxdNetworkState } from "types/network";
import type {
LxdNetwork,
LXDNetworkOnClusterMember,
LxdNetworkState,
} from "types/network";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdClusterMember } from "types/cluster";
import { areNetworksEqual } from "util/networks";
import type { ClusterSpecificValues } from "components/ClusterSpecificSelect";
import type { LxdClusterMember } from "types/cluster";

export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
export const fetchNetworks = (
project: string,
target?: string,
): Promise<LxdNetwork[]> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks?project=${project}&recursion=1`)
fetch(`/1.0/networks?project=${project}&recursion=1${targetParam}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdNetwork[]>) => {
const filteredNetworks = data.metadata.filter(
Expand All @@ -20,24 +29,97 @@ export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
});
};

const constructMemberError = (
result: PromiseRejectedResult,
member: string,
) => {
const reason = result.reason as Error;
const message = `Error from cluster member ${member}: ${reason.message}`;
return new Error(message);
};

export const fetchNetworksFromClusterMembers = (
project: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDNetworkOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchNetworks(project, member.server_name);
}),
)
.then((results) => {
const networksOnMembers: LXDNetworkOnClusterMember[] = [];
for (let i = 0; i < clusterMembers.length; i++) {
const memberName = clusterMembers[i].server_name;
const result = results[i];
if (result.status === "rejected") {
reject(constructMemberError(result, memberName));
}
if (result.status === "fulfilled") {
result.value.forEach((network) =>
networksOnMembers.push({ ...network, memberName }),
);
}
}
resolve(networksOnMembers);
})
.catch(reject);
});
};

export const fetchNetwork = (
name: string,
project: string,
target?: string,
): Promise<LxdNetwork> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${name}?project=${project}`)
fetch(`/1.0/networks/${name}?project=${project}${targetParam}`)
.then(handleEtagResponse)
.then((data) => resolve(data as LxdNetwork))
.catch(reject);
});
};

export const fetchNetworkFromClusterMembers = (
name: string,
project: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDNetworkOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchNetwork(name, project, member.server_name);
}),
)
.then((results) => {
const networkOnMembers: LXDNetworkOnClusterMember[] = [];
for (let i = 0; i < clusterMembers.length; i++) {
const memberName = clusterMembers[i].server_name;
const result = results[i];
if (result.status === "rejected") {
reject(constructMemberError(result, memberName));
}
if (result.status === "fulfilled") {
const promise = results[i] as PromiseFulfilledResult<LxdNetwork>;
networkOnMembers.push({ ...promise.value, memberName: memberName });
}
}
resolve(networkOnMembers);
})
.catch(reject);
});
};

export const fetchNetworkState = (
name: string,
project: string,
target?: string,
): Promise<LxdNetworkState> => {
const targetParam = target ? `&target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${name}/state?project=${project}`)
fetch(`/1.0/networks/${name}/state?project=${project}${targetParam}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdNetworkState>) => resolve(data.metadata))
.catch(reject);
Expand All @@ -48,20 +130,19 @@ export const createClusterNetwork = (
network: Partial<LxdNetwork>,
project: string,
clusterMembers: LxdClusterMember[],
parentsPerClusterMember?: ClusterSpecificValues,
): Promise<void> => {
return new Promise((resolve, reject) => {
const memberNetwork = {
name: network.name,
description: network.description,
type: network.type,
config: {
parent: network.config?.parent,
},
};

Promise.allSettled(
clusterMembers.map(async (member) => {
await createNetwork(memberNetwork, project, member.server_name);
clusterMembers.map((member) => {
const memberNetwork = {
name: network.name,
type: network.type,
config: {
parent: parentsPerClusterMember?.[member.server_name],
},
};
return createNetwork(memberNetwork, project, member.server_name);
}),
)
.then((results) => {
Expand All @@ -72,6 +153,7 @@ export const createClusterNetwork = (
reject(error);
return;
}

// The network parent is cluster member specific, so we omit it on the cluster wide network configuration.
delete network.config?.parent;
createNetwork(network, project).then(resolve).catch(reject);
Expand Down Expand Up @@ -110,15 +192,20 @@ export const createNetwork = (
export const updateNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
target?: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/networks/${network.name ?? ""}?project=${project}`, {
method: "PUT",
body: JSON.stringify(network),
headers: {
"If-Match": network.etag ?? "invalid-etag",
const targetParam = target ? `&target=${target}` : "";
fetch(
`/1.0/networks/${network.name ?? ""}?project=${project}${targetParam}`,
{
method: "PUT",
body: JSON.stringify(network),
headers: {
"If-Match": network.etag ?? "",
},
},
})
)
.then(handleResponse)
.then(resolve)
.catch(async (e: Error) => {
Expand All @@ -135,6 +222,44 @@ export const updateNetwork = (
});
};

export const updateClusterNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
parentsPerClusterMember?: ClusterSpecificValues,
): Promise<void> => {
if (!parentsPerClusterMember) {
return updateNetwork(network, project);
}

return new Promise((resolve, reject) => {
Promise.allSettled(
Object.keys(parentsPerClusterMember).map((memberName) => {
const memberNetwork = {
name: network.name,
type: network.type,
config: {
parent: parentsPerClusterMember[memberName],
},
};
return updateNetwork(memberNetwork, project, memberName);
}),
)
.then((results) => {
const error = results.find((res) => res.status === "rejected")
?.reason as Error | undefined;

if (error) {
reject(error);
return;
}
updateNetwork({ ...network, etag: "" }, project)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
};

export const renameNetwork = (
oldName: string,
newName: string,
Expand Down
Loading

0 comments on commit b52956e

Please sign in to comment.