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

Network clustering #1053

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[]> => {
edlerd marked this conversation as resolved.
Show resolved Hide resolved
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
Loading