|
| 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 | +} |
0 commit comments