Skip to content

Commit dcce1e5

Browse files
committed
compute server: implement clone feature
1 parent b24cae4 commit dcce1e5

File tree

3 files changed

+128
-0
lines changed

3 files changed

+128
-0
lines changed

src/packages/frontend/compute/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export async function computeServerAction(opts: {
3737
await api("compute/compute-server-action", opts);
3838
}
3939

40+
export async function getServers(opts: { id?: number; project_id: string }) {
41+
return await api("compute/get-servers", opts);
42+
}
43+
4044
export async function getServerState(id: number) {
4145
return await api("compute/get-server-state", { id });
4246
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Clone compute server config. Entirely done client side.
3+
4+
Main issue is DNS can't be the same.
5+
6+
In the future we will ALSO support a checkbox to clone the data too, but not yet.
7+
*/
8+
9+
import { Alert, Modal } from "antd";
10+
import { useState } from "react";
11+
import ShowError from "@cocalc/frontend/components/error";
12+
import Inline from "./inline";
13+
import { createServer, getServers } from "./api";
14+
15+
export default function Clone({ id, project_id, close }) {
16+
const [error, setError] = useState<string>("");
17+
const [loading, setLoading] = useState<boolean>(false);
18+
19+
return (
20+
<Modal
21+
open
22+
confirmLoading={loading}
23+
onCancel={close}
24+
onOk={async () => {
25+
try {
26+
setLoading(true);
27+
await createClone({ id, project_id });
28+
close();
29+
} catch (err) {
30+
setError(`${err}`);
31+
} finally {
32+
setLoading(false);
33+
}
34+
}}
35+
title={
36+
<>
37+
Clone Compute Server <Inline id={id} />
38+
</>
39+
}
40+
okText={
41+
<>
42+
Clon{loading ? "ing" : "e"} <Inline id={id} />
43+
</>
44+
}
45+
>
46+
<ShowError
47+
error={error}
48+
setError={setError}
49+
style={{ marginBottom: "15px" }}
50+
/>
51+
This makes a new deprovisioned compute server that is configured as close
52+
as possibleto this this compute server.{" "}
53+
<Alert
54+
showIcon
55+
style={{ margin: "15px" }}
56+
type="warning"
57+
message="The underlying disk is not copied."
58+
/>
59+
After cloning the compute server, you can edit anything about its
60+
configuration before starting it.
61+
</Modal>
62+
);
63+
}
64+
65+
async function createClone({
66+
id,
67+
project_id,
68+
}: {
69+
id: number;
70+
project_id: string;
71+
}) {
72+
const servers = await getServers({ project_id });
73+
const titles = new Set(servers.map((x) => x.title));
74+
const allDns = new Set(
75+
servers.filter((x) => x.configuration.dns).map((x) => x.configuration.dns),
76+
);
77+
let server;
78+
let done = false;
79+
for (const s of servers) {
80+
if (s.id == id) {
81+
server = s;
82+
done = true;
83+
break;
84+
}
85+
}
86+
if (!done) {
87+
throw Error(`no such compute server ${id}`);
88+
}
89+
let n = 1;
90+
let title = `Clone of ${server.title}`;
91+
if (titles.has(title)) {
92+
while (titles.has(title + ` (${n})`)) {
93+
n += 1;
94+
}
95+
title = title + ` (${n})`;
96+
}
97+
server.title = title;
98+
99+
delete server.configuration.authToken;
100+
101+
if (server.configuration.dns) {
102+
n = 1;
103+
while (allDns.has(server.configuration.dns + `-${n}`)) {
104+
n += 1;
105+
}
106+
server.configuration.dns = server.configuration.dns + `-${n}`;
107+
}
108+
109+
await createServer({ ...server });
110+
}

src/packages/frontend/compute/menu.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { setServerConfiguration } from "@cocalc/frontend/compute/api";
1717
import ShowError from "@cocalc/frontend/components/error";
1818
import openSupportTab from "@cocalc/frontend/support/open";
1919
import { setTemplate } from "@cocalc/frontend/compute/api";
20+
import CloneModal from "./clone";
2021

2122
function getServer({ id, project_id }) {
2223
return redux
@@ -259,6 +260,12 @@ function getItems({
259260
label: is_owner ? "Settings" : "Details...",
260261
};
261262

263+
const clone = {
264+
key: "clone",
265+
icon: <Icon name="copy" />,
266+
label: "Clone...",
267+
};
268+
262269
return [
263270
titleAndColor,
264271
// {
@@ -287,6 +294,7 @@ function getItems({
287294
},
288295
settings,
289296
options,
297+
clone,
290298
{
291299
type: "divider",
292300
},
@@ -439,6 +447,12 @@ export default function Menu({
439447
);
440448
break;
441449

450+
case "clone":
451+
setModal(
452+
<CloneModal id={id} project_id={project_id} close={close} />,
453+
);
454+
break;
455+
442456
case "serial-console-log":
443457
setModal(
444458
<SerialLogModal

0 commit comments

Comments
 (0)