Files
WebAndAppMonoRepo/output/internal/services/cloudron_service.go
YourDreamNameHere 89443f213b feat: implement core Go application with web server
- 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
2025-11-20 16:36:28 -05:00

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
}