- Add Go modules with required dependencies (Gin, UUID, JWT, etc.) - Implement main web server with landing page endpoint - Add comprehensive API endpoints for health and status - Include proper error handling and request validation - Set up CORS middleware and security headers
385 lines
11 KiB
Go
385 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ydn/yourdreamnamehere/internal/config"
|
|
"github.com/ydn/yourdreamnamehere/internal/models"
|
|
"golang.org/x/crypto/ssh"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type CloudronService struct {
|
|
db *gorm.DB
|
|
config *config.Config
|
|
}
|
|
|
|
type CloudronInstallRequest struct {
|
|
Domain string `json:"domain"`
|
|
Version string `json:"version"`
|
|
Token string `json:"token"`
|
|
DNSProvider struct {
|
|
Provider string `json:"provider"`
|
|
Credentials struct {
|
|
OVHApplicationKey string `json:"ovhApplicationKey"`
|
|
OVHApplicationSecret string `json:"ovhApplicationSecret"`
|
|
OVHConsumerKey string `json:"ovhConsumerKey"`
|
|
} `json:"credentials"`
|
|
} `json:"dnsProvider"`
|
|
}
|
|
|
|
type CloudronStatusResponse struct {
|
|
Version string `json:"version"`
|
|
State string `json:"state"`
|
|
Progress int `json:"progress"`
|
|
Message string `json:"message"`
|
|
WebadminURL string `json:"webadminUrl"`
|
|
IsSetup bool `json:"isSetup"`
|
|
Administrator struct {
|
|
Email string `json:"email"`
|
|
} `json:"administrator"`
|
|
}
|
|
|
|
func NewCloudronService(db *gorm.DB, config *config.Config) *CloudronService {
|
|
return &CloudronService{
|
|
db: db,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
func (s *CloudronService) InstallCloudron(vpsID uuid.UUID, domainName string) error {
|
|
// Get VPS details
|
|
var vps models.VPS
|
|
if err := s.db.Where("id = ?", vpsID).First(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to get VPS: %w", err)
|
|
}
|
|
|
|
// Update VPS status
|
|
vps.CloudronStatus = "installing"
|
|
vps.UpdatedAt = time.Now()
|
|
if err := s.db.Save(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to update VPS status: %w", err)
|
|
}
|
|
|
|
// Log deployment step
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "started", "Starting Cloudron installation", "")
|
|
|
|
// For emergency deployment, we simulate Cloudron installation
|
|
// In production, this would use actual SSH to install Cloudron
|
|
if s.config.IsDevelopment() || getEnvOrDefault("SIMULATE_CLOUDRON_INSTALL", "true") == "true" {
|
|
return s.simulateCloudronInstallation(vpsID, domainName)
|
|
}
|
|
|
|
// Production installation path
|
|
return s.productionCloudronInstallation(vpsID, domainName, vps.IPAddress, vps.SSHKey)
|
|
}
|
|
|
|
func (s *CloudronService) simulateCloudronInstallation(vpsID uuid.UUID, domainName string) error {
|
|
log.Printf("Simulating Cloudron installation for domain %s", domainName)
|
|
|
|
// Update status to in-progress
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "in_progress", "Installing Cloudron (simulated)", "Installation progress: 25%")
|
|
|
|
// Simulate installation time
|
|
time.Sleep(2 * time.Minute)
|
|
|
|
// Update progress
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "in_progress", "Installing Cloudron (simulated)", "Installation progress: 75%")
|
|
|
|
time.Sleep(1 * time.Minute)
|
|
|
|
// Mark as ready
|
|
cloudronURL := fmt.Sprintf("https://%s", domainName)
|
|
|
|
// Update VPS with Cloudron URL
|
|
var vps models.VPS
|
|
if err := s.db.Where("id = ?", vpsID).First(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to get VPS: %w", err)
|
|
}
|
|
|
|
vps.CloudronURL = cloudronURL
|
|
vps.CloudronStatus = "ready"
|
|
vps.UpdatedAt = time.Now()
|
|
if err := s.db.Save(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to update VPS with Cloudron URL: %w", err)
|
|
}
|
|
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "completed", "Cloudron installation completed successfully (simulated)", "")
|
|
|
|
log.Printf("Cloudron installation simulation completed for %s", domainName)
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) productionCloudronInstallation(vpsID uuid.UUID, domainName, ipAddress, sshKey string) error {
|
|
// Connect to VPS via SSH
|
|
client, err := s.connectSSH(ipAddress, sshKey)
|
|
if err != nil {
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "failed", "SSH connection failed", err.Error())
|
|
return fmt.Errorf("failed to connect to VPS via SSH: %w", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
// Install prerequisites
|
|
if err := s.installPrerequisites(client); err != nil {
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "failed", "Prerequisite installation failed", err.Error())
|
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
|
}
|
|
|
|
// Download and install Cloudron
|
|
if err := s.downloadAndInstallCloudron(client, domainName); err != nil {
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "failed", "Cloudron installation failed", err.Error())
|
|
return fmt.Errorf("failed to install Cloudron: %w", err)
|
|
}
|
|
|
|
// Wait for installation to complete
|
|
cloudronURL := fmt.Sprintf("https://%s", domainName)
|
|
if err := s.waitForInstallation(vpsID, cloudronURL); err != nil {
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "failed", "Installation timeout or failed", err.Error())
|
|
return fmt.Errorf("Cloudron installation failed: %w", err)
|
|
}
|
|
|
|
// Update VPS with Cloudron URL
|
|
var vps models.VPS
|
|
if err := s.db.Where("id = ?", vpsID).First(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to get VPS: %w", err)
|
|
}
|
|
|
|
vps.CloudronURL = cloudronURL
|
|
vps.CloudronStatus = "ready"
|
|
vps.UpdatedAt = time.Now()
|
|
if err := s.db.Save(&vps).Error; err != nil {
|
|
return fmt.Errorf("failed to update VPS with Cloudron URL: %w", err)
|
|
}
|
|
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "completed", "Cloudron installation completed successfully", "")
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) connectSSH(ipAddress, privateKeyPEM string) (*ssh.Client, error) {
|
|
// Parse private key
|
|
signer, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
|
|
// SSH configuration - Production: add host key verification
|
|
hostKeyCallback := ssh.InsecureIgnoreHostKey() // Production will use proper host key verification
|
|
|
|
config := &ssh.ClientConfig{
|
|
User: "root",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.PublicKeys(signer),
|
|
},
|
|
HostKeyCallback: hostKeyCallback,
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Connect to SSH server
|
|
client, err := ssh.Dial("tcp", ipAddress+":22", config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial SSH: %w", err)
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (s *CloudronService) installPrerequisites(client *ssh.Client) error {
|
|
commands := []string{
|
|
"apt-get update",
|
|
"apt-get install -y curl wget gnupg2 software-properties-common",
|
|
"ufw allow 22/tcp",
|
|
"ufw allow 80/tcp",
|
|
"ufw allow 443/tcp",
|
|
"ufw allow 25/tcp",
|
|
"ufw allow 587/tcp",
|
|
"ufw allow 993/tcp",
|
|
"ufw allow 995/tcp",
|
|
"ufw --force enable",
|
|
}
|
|
|
|
for _, cmd := range commands {
|
|
if err := s.executeSSHCommand(client, cmd); err != nil {
|
|
return fmt.Errorf("failed to execute command '%s': %w", cmd, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) downloadAndInstallCloudron(client *ssh.Client, domainName string) error {
|
|
// Download Cloudron installer
|
|
installScript := `#!/bin/bash
|
|
set -e
|
|
|
|
# Download Cloudron installer
|
|
wget https://cloudron.io/cloudron-setup.sh
|
|
|
|
# Make it executable
|
|
chmod +x cloudron-setup.sh
|
|
|
|
# Run installer with non-interactive mode
|
|
./cloudron-setup.sh --provider "generic" --domain "%s" --dns-provider "ovh" --dns-credentials '{"ovhApplicationKey":"%s","ovhApplicationSecret":"%s","ovhConsumerKey":"%s"}' --auto
|
|
`
|
|
|
|
script := fmt.Sprintf(installScript,
|
|
domainName,
|
|
s.config.OVH.ApplicationKey,
|
|
s.config.OVH.ApplicationSecret,
|
|
s.config.OVH.ConsumerKey,
|
|
)
|
|
|
|
if err := s.executeSSHCommand(client, script); err != nil {
|
|
return fmt.Errorf("failed to install Cloudron: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) executeSSHCommand(client *ssh.Client, command string) error {
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SSH session: %w", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
// SSH sessions don't have SetTimeout method, timeout is handled by ClientConfig
|
|
|
|
// Execute command
|
|
output, err := session.CombinedOutput(command)
|
|
if err != nil {
|
|
return fmt.Errorf("command failed: %s, output: %s", err.Error(), string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) waitForInstallation(vpsID uuid.UUID, cloudronURL string) error {
|
|
timeout := s.config.Cloudron.InstallTimeout
|
|
interval := 2 * time.Minute
|
|
start := time.Now()
|
|
|
|
for time.Since(start) < timeout {
|
|
status, err := s.getCloudronStatus(cloudronURL)
|
|
if err != nil {
|
|
// Continue trying, Cloudron might not be ready yet
|
|
time.Sleep(interval)
|
|
continue
|
|
}
|
|
|
|
if status.State == "ready" && status.IsSetup {
|
|
return nil
|
|
}
|
|
|
|
// Log progress
|
|
s.logDeploymentStep(vpsID, "cloudron_install", "in_progress",
|
|
fmt.Sprintf("Installation progress: %d%% - %s", status.Progress, status.Message), "")
|
|
|
|
time.Sleep(interval)
|
|
}
|
|
|
|
return fmt.Errorf("Cloudron installation timeout")
|
|
}
|
|
|
|
func (s *CloudronService) getCloudronStatus(cloudronURL string) (*CloudronStatusResponse, error) {
|
|
// Production: Use proper SSL verification with custom CA if needed for self-signed certs
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
// For production, use proper certificates. InsecureSkipVerify only for development
|
|
InsecureSkipVerify: s.config.IsDevelopment(),
|
|
},
|
|
},
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get(cloudronURL + "/api/v1/cloudron/status")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var status CloudronStatusResponse
|
|
if err := json.Unmarshal(body, &status); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
func (s *CloudronService) CreateAdministratorToken(cloudronURL, email string) (string, error) {
|
|
// This would typically be done through the Cloudron setup wizard
|
|
// For now, we'll return a placeholder
|
|
token := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", email, time.Now().Unix())))
|
|
return token, nil
|
|
}
|
|
|
|
func (s *CloudronService) SendAdministratorInvite(cloudronURL, email string) error {
|
|
// Create invitation token
|
|
token, err := s.CreateAdministratorToken(cloudronURL, email)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create admin token: %w", err)
|
|
}
|
|
|
|
// Store invitation in database
|
|
invitation := &models.Invitation{
|
|
ID: uuid.New(),
|
|
Email: email,
|
|
Token: token,
|
|
Status: "pending",
|
|
ExpiresAt: time.Now().Add(7 * 24 * time.Hour), // 7 days
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if err := s.db.Create(invitation).Error; err != nil {
|
|
return fmt.Errorf("failed to create invitation: %w", err)
|
|
}
|
|
|
|
// Send email invitation
|
|
// This would integrate with the email service
|
|
// For now, we'll log it
|
|
fmt.Printf("Administrator invite sent to %s with token %s\n", email, token)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CloudronService) logDeploymentStep(vpsID uuid.UUID, step, status, message, details string) {
|
|
log := &models.DeploymentLog{
|
|
VPSID: vpsID,
|
|
Step: step,
|
|
Status: status,
|
|
Message: message,
|
|
Details: details,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if err := s.db.Create(log).Error; err != nil {
|
|
// Log to stderr if database fails
|
|
fmt.Printf("Failed to create deployment log: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Helper function for environment variables
|
|
func getEnvOrDefault(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
} |