Skip to content

Commit fa37252

Browse files
authored
Merge pull request #10 from OpenSourceAGI/claude/ssh-key-localstorage-Peto7
2 parents 07ec402 + 9c6b3d4 commit fa37252

File tree

11 files changed

+687
-39
lines changed

11 files changed

+687
-39
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { type NextRequest, NextResponse } from "next/server"
2+
import { Client } from "ssh2"
3+
4+
export const runtime = "nodejs"
5+
export const dynamic = "force-dynamic"
6+
7+
interface SSHExecuteResult {
8+
stdout: string
9+
stderr: string
10+
code: number
11+
}
12+
13+
async function executeSSHCommand(
14+
host: string,
15+
privateKey: string,
16+
command: string,
17+
username: string = "ubuntu"
18+
): Promise<SSHExecuteResult> {
19+
return new Promise((resolve, reject) => {
20+
const conn = new Client()
21+
let stdout = ""
22+
let stderr = ""
23+
24+
conn
25+
.on("ready", () => {
26+
console.log("[SSH] Connection established")
27+
conn.exec(command, (err, stream) => {
28+
if (err) {
29+
conn.end()
30+
return reject(err)
31+
}
32+
33+
stream
34+
.on("close", (code: number) => {
35+
console.log("[SSH] Command completed with code:", code)
36+
conn.end()
37+
resolve({ stdout, stderr, code })
38+
})
39+
.on("data", (data: Buffer) => {
40+
const output = data.toString()
41+
stdout += output
42+
console.log("[SSH] STDOUT:", output)
43+
})
44+
.stderr.on("data", (data: Buffer) => {
45+
const output = data.toString()
46+
stderr += output
47+
console.log("[SSH] STDERR:", output)
48+
})
49+
})
50+
})
51+
.on("error", (err) => {
52+
console.error("[SSH] Connection error:", err)
53+
reject(err)
54+
})
55+
.connect({
56+
host,
57+
port: 22,
58+
username,
59+
privateKey: privateKey,
60+
readyTimeout: 30000,
61+
timeout: 30000,
62+
})
63+
})
64+
}
65+
66+
export async function POST(req: NextRequest) {
67+
try {
68+
const body = await req.json()
69+
const {
70+
instanceId,
71+
publicIp,
72+
keyName,
73+
privateKey,
74+
installDokploy,
75+
dokployApiKey,
76+
installDevToolsShell,
77+
dockerServices,
78+
githubRepos,
79+
customScript,
80+
} = body
81+
82+
if (!publicIp) {
83+
return NextResponse.json({ error: "Public IP is required" }, { status: 400 })
84+
}
85+
86+
if (!privateKey) {
87+
return NextResponse.json({ error: "SSH private key is required" }, { status: 400 })
88+
}
89+
90+
// Build installation description for display
91+
const installing: string[] = []
92+
if (installDokploy) installing.push("Dokploy")
93+
if (installDevToolsShell) installing.push("Dev Tools Shell")
94+
if (dockerServices?.length > 0) installing.push(`${dockerServices.length} Docker service(s)`)
95+
if (githubRepos?.length > 0) installing.push(`${githubRepos.length} GitHub repo(s)`)
96+
if (customScript?.trim()) installing.push("Custom script")
97+
98+
// Build the installation script
99+
let installScript = "#!/bin/bash\n\n"
100+
installScript += "set -e\n\n"
101+
installScript += "echo 'Starting software installation...'\n\n"
102+
103+
// Install Dokploy
104+
if (installDokploy) {
105+
installScript += "# Install Docker if not already installed\n"
106+
installScript += "if ! command -v docker &> /dev/null; then\n"
107+
installScript += " curl -fsSL https://get.docker.com -o get-docker.sh\n"
108+
installScript += " sh get-docker.sh\n"
109+
installScript += " rm get-docker.sh\n"
110+
installScript += "fi\n\n"
111+
112+
installScript += "# Install Docker Compose if not already installed\n"
113+
installScript += "if ! command -v docker-compose &> /dev/null; then\n"
114+
installScript += ' curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose\n'
115+
installScript += " chmod +x /usr/local/bin/docker-compose\n"
116+
installScript += "fi\n\n"
117+
118+
installScript += "# Initialize Docker Swarm if not already initialized\n"
119+
installScript += "if ! docker info | grep -q 'Swarm: active'; then\n"
120+
installScript += " docker swarm init\n"
121+
installScript += "fi\n\n"
122+
123+
installScript += "# Install Dokploy\n"
124+
installScript += "curl -sSL https://dokploy.com/install.sh | sh\n\n"
125+
}
126+
127+
if (installDevToolsShell) {
128+
installScript += "# Install comprehensive dev tools shell\n"
129+
installScript += "echo 'Installing Fish, Neovim, Nushell, Bun, Node, Helix, Starship, and more...'\n"
130+
installScript += "wget -qO- https://dub.sh/dev.sh | bash -s -- all\n"
131+
installScript += "echo 'Dev tools shell installation completed'\n\n"
132+
}
133+
134+
// Setup Docker Compose for new services
135+
if (dockerServices && dockerServices.length > 0) {
136+
installScript += "# Setup Docker Compose\n"
137+
installScript += "cd /root\n"
138+
installScript += "cat > docker-compose-addon.yml << 'EOFCOMPOSE'\n"
139+
installScript += "version: '3.8'\n"
140+
installScript += "services:\n"
141+
142+
dockerServices.forEach((service: any) => {
143+
installScript += ` ${service.name}:\n`
144+
installScript += ` image: ${service.image}\n`
145+
installScript += ` restart: unless-stopped\n`
146+
if (service.ports && service.ports.length > 0) {
147+
installScript += ` ports:\n`
148+
service.ports.forEach((port: string) => {
149+
installScript += ` - "${port}"\n`
150+
})
151+
}
152+
installScript += "\n"
153+
})
154+
155+
installScript += "EOFCOMPOSE\n\n"
156+
installScript += "docker-compose -f docker-compose-addon.yml up -d\n\n"
157+
}
158+
159+
// Clone and deploy GitHub repos
160+
if (githubRepos && githubRepos.length > 0) {
161+
installScript += "# Clone GitHub repositories\n"
162+
installScript += "mkdir -p /root/repos\n"
163+
installScript += "cd /root/repos\n\n"
164+
165+
githubRepos.forEach((repo: any) => {
166+
const repoName = repo.name.split("/").pop()
167+
installScript += `git clone ${repo.url} ${repoName}\n`
168+
})
169+
170+
if (installDokploy && dokployApiKey) {
171+
installScript += "\n# Deploy to Dokploy via API\n"
172+
installScript += "sleep 30\n"
173+
githubRepos.forEach((repo: any) => {
174+
installScript += `echo "Deploying ${repo.name} to Dokploy..."\n`
175+
installScript += `# You can add Dokploy API calls here when available\n`
176+
})
177+
}
178+
179+
installScript += "\n"
180+
}
181+
182+
// Add custom script
183+
if (customScript && customScript.trim()) {
184+
installScript += "# Custom script\n"
185+
installScript += customScript + "\n\n"
186+
}
187+
188+
installScript += "echo 'Software installation completed!'\n"
189+
190+
console.log("[SSH] Generated installation script for instance:", instanceId)
191+
console.log("[SSH] Installing:", installing.join(", "))
192+
console.log("[SSH] Connecting to:", publicIp)
193+
194+
try {
195+
// Execute the script via SSH
196+
const result = await executeSSHCommand(publicIp, privateKey, installScript)
197+
198+
console.log("[SSH] Command execution completed")
199+
console.log("[SSH] Exit code:", result.code)
200+
201+
if (result.code === 0) {
202+
return NextResponse.json({
203+
success: true,
204+
installing: installing.join(", "),
205+
message: "Installation completed successfully via SSH",
206+
output: result.stdout,
207+
})
208+
} else {
209+
return NextResponse.json(
210+
{
211+
success: false,
212+
error: "Installation failed",
213+
message: `Installation script exited with code ${result.code}`,
214+
stdout: result.stdout,
215+
stderr: result.stderr,
216+
},
217+
{ status: 500 }
218+
)
219+
}
220+
} catch (sshError: any) {
221+
console.error("[SSH] Execution failed:", sshError)
222+
223+
return NextResponse.json(
224+
{
225+
success: false,
226+
error: "SSH_CONNECTION_FAILED",
227+
message: `Failed to connect via SSH: ${sshError.message}`,
228+
script: installScript,
229+
installing: installing.join(", "),
230+
},
231+
{ status: 500 }
232+
)
233+
}
234+
} catch (error: any) {
235+
console.error("[SSH] Error installing software:", error)
236+
return NextResponse.json({ error: error.message || "Failed to install software" }, { status: 500 })
237+
}
238+
}

apps/Cloud-Computer-Control-Panel/app/api/servers/create/route.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ export const runtime = "nodejs"
44
export const dynamic = "force-dynamic"
55

66
import { NextResponse } from "next/server"
7-
import { runInstance, allocateAddress, associateAddress, createOrGetDokploySecurityGroup } from "@/lib/aws-ec2-client"
7+
import { runInstance, allocateAddress, associateAddress, createOrGetDokploySecurityGroup, importKeyPair } from "@/lib/aws-ec2-client"
88
import { getUbuntuAMI } from "@/lib/aws-ami-ids"
9+
import { generateSSHKeyPair } from "@/lib/ssh-key-utils"
910

1011
export async function POST(request: Request) {
1112
try {
@@ -222,23 +223,26 @@ ${config.customScript}
222223
echo "Setup completed at $(date)" > /var/log/setup-complete.log
223224
`
224225

225-
const cleanKeyName = config.keyName && config.keyName.trim() !== "" ? config.keyName : undefined
226-
console.log("[v0] Launching with keyName:", cleanKeyName || "none (will launch without key pair)")
226+
// Generate SSH key pair and import to AWS
227+
console.log("[v0] Generating SSH key pair...")
228+
const sshKeyPair = generateSSHKeyPair(2048)
229+
230+
// Create a unique key name based on instance name and timestamp
231+
const keyName = `${config.instanceName.replace(/[^a-zA-Z0-9-]/g, '-')}-${Date.now()}`
232+
233+
console.log("[v0] Importing SSH public key to AWS as:", keyName)
234+
await importKeyPair(accessKeyId, secretAccessKey, region, keyName, sshKeyPair.publicKey)
235+
236+
console.log("[v0] Launching instance with SSH key:", keyName)
227237

228238
const { instanceId: newInstanceId } = await runInstance(accessKeyId, secretAccessKey, region, {
229239
imageId: getUbuntuAMI(region),
230240
instanceType: config.instanceType || "t3.small",
231-
keyName: cleanKeyName,
241+
keyName: keyName,
232242
storageSize: config.storageSize || 40,
233243
instanceName: config.instanceName,
234244
userDataScript,
235245
securityGroupId,
236-
tags: config.setupDokploy
237-
? [
238-
{ Key: "Name", Value: config.instanceName },
239-
{ Key: "Dokploy", Value: "true" },
240-
]
241-
: [{ Key: "Name", Value: config.instanceName }],
242246
})
243247

244248
if (!newInstanceId) {
@@ -257,19 +261,37 @@ echo "Setup completed at $(date)" > /var/log/setup-complete.log
257261
instanceId: newInstanceId,
258262
elasticIp: publicIp,
259263
allocationId,
264+
sshKey: {
265+
keyName: keyName,
266+
privateKey: sshKeyPair.privateKey,
267+
publicKey: sshKeyPair.publicKey,
268+
fingerprint: sshKeyPair.fingerprint,
269+
},
260270
})
261271
}
262272
} catch (ipError) {
263273
console.error("[v0] Failed to allocate Elastic IP:", ipError)
264274
return NextResponse.json({
265275
message: "Instance launched but Elastic IP allocation failed",
266276
instanceId: newInstanceId,
277+
sshKey: {
278+
keyName: keyName,
279+
privateKey: sshKeyPair.privateKey,
280+
publicKey: sshKeyPair.publicKey,
281+
fingerprint: sshKeyPair.fingerprint,
282+
},
267283
})
268284
}
269285

270286
return NextResponse.json({
271287
message: "Instance launched successfully",
272288
instanceId: newInstanceId,
289+
sshKey: {
290+
keyName: keyName,
291+
privateKey: sshKeyPair.privateKey,
292+
publicKey: sshKeyPair.publicKey,
293+
fingerprint: sshKeyPair.fingerprint,
294+
},
273295
})
274296
} catch (error) {
275297
console.error("[v0] Error creating server:", error)

0 commit comments

Comments
 (0)