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 20, 2025
1 parent adf3ed9 commit 0bc7a06
Show file tree
Hide file tree
Showing 38 changed files with 1,294 additions and 613 deletions.
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@
"@monaco-editor/react": "4.6.0",
"@tanstack/react-query": "5.63.0",
"axios": "1.7.9",
"cytoscape": "3.30.4",
"cytoscape-popper": "4.0.1",
"formik": "2.4.6",
"js-yaml": "4.1.0",
"lodash.isequal": "4.5.0",
"node-forge": "1.3.1",
"parse-prometheus-text-format": "1.1.1",
"react": "18.3.1",
"react-cytoscapejs": "2.0.0",
"react-dom": "18.3.1",
"react-router-dom": "7.1.1",
"react-useportal": "1.0.19",
Expand All @@ -59,7 +56,6 @@
"@canonical/typescript-config-react": "0.4.0-experimental.0",
"@playwright/test": "1.49.1",
"@types/convert-source-map": "2.0.3",
"@types/cytoscape-popper": "2.0.4",
"@types/dotenv": "8.2.3",
"@types/lodash.isequal": "4.5.8",
"@types/node-forge": "1.3.11",
Expand Down
17 changes: 8 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const InstanceList = lazy(() => import("pages/instances/InstanceList"));
const Login = lazy(() => import("pages/login/Login"));
const NetworkDetail = lazy(() => import("pages/networks/NetworkDetail"));
const NetworkList = lazy(() => import("./pages/networks/NetworkList"));
const NetworkMap = lazy(() => import("pages/networks/NetworkMap"));
const OperationList = lazy(() => import("pages/operations/OperationList"));
const ProfileDetail = lazy(() => import("pages/profiles/ProfileDetail"));
const ProfileList = lazy(() => import("pages/profiles/ProfileList"));
Expand Down Expand Up @@ -219,42 +218,42 @@ const App: FC = () => {
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab"
path="/ui/project/:project/member/:member/network/:name"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab/:section"
path="/ui/project/:project/network/:name/:activeTab"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:network/forwards/create"
path="/ui/project/:project/network/:name/:activeTab/:section"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<CreateNetworkForward />} />}
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:network/forwards/:forwardAddress/edit"
path="/ui/project/:project/network/:network/forwards/create"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<EditNetworkForward />} />}
outlet={<ProjectLoader outlet={<CreateNetworkForward />} />}
/>
}
/>
<Route
path="/ui/project/:project/networks/map"
path="/ui/project/:project/network/:network/forwards/:forwardAddress/edit"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkMap />} />}
outlet={<ProjectLoader outlet={<EditNetworkForward />} />}
/>
}
/>
Expand Down
167 changes: 144 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,40 @@ export const updateNetwork = (
});
};

export const updateClusterNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
parentsPerClusterMember: ClusterSpecificValues,
): Promise<void> => {
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 0bc7a06

Please sign in to comment.