service.go

154 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
//go:build cgo

package commands

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const serviceUsage = `Manage services in Congo Dev

Usage:
  congo service <subcommand>

Subcommands:
  list                    List services (JSON)
  deploy <repo> [name]    Build and deploy a service from a repo
  stop <repo> <name>      Stop a service
  start <repo> <name>     Start a stopped service
  status <repo> <name>    Check service status
`

func Service() {
	if len(os.Args) < 3 {
		fmt.Print(serviceUsage)
		os.Exit(1)
	}

	switch os.Args[2] {
	case "list":
		// List running containers on the internal network
		out, _ := exec.Command("docker", "ps", "--format", "{{json .}}").Output()
		fmt.Println("[")
		lines := strings.Split(strings.TrimSpace(string(out)), "\n")
		for i, line := range lines {
			if line == "" { continue }
			if i > 0 { fmt.Println(",") }
			fmt.Print(line)
		}
		fmt.Println("\n]")
	case "deploy":
		serviceDeploy()
	case "stop":
		serviceControl("stop")
	case "start":
		serviceControl("start")
	case "status":
		serviceStatus()
	default:
		fmt.Fprintf(os.Stderr, "unknown service subcommand %q\n", os.Args[2])
		os.Exit(1)
	}
}

func serviceDeploy() {
	if len(os.Args) < 4 {
		fmt.Fprintln(os.Stderr, "usage: congo service deploy <repo> [service-name]")
		os.Exit(1)
	}
	repoName := os.Args[3]
	serviceName := "app"
	if len(os.Args) > 4 {
		serviceName = os.Args[4]
	}

	repoPath := "/home/coder/repos/" + repoName
	if _, err := os.Stat(repoPath); os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "repo %s not found at %s\n", repoName, repoPath)
		os.Exit(1)
	}

	imageName := fmt.Sprintf("congo-%s-%s:latest", repoName, serviceName)
	containerName := fmt.Sprintf("%s-%s", repoName, serviceName)

	// Build Docker image
	fmt.Printf("Building %s...\n", imageName)
	buildCmd := exec.Command("docker", "build", "-t", imageName, "-f",
		filepath.Join(repoPath, "Dockerfile"), repoPath)
	buildCmd.Stdout = os.Stdout
	buildCmd.Stderr = os.Stderr
	if err := buildCmd.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "docker build failed: %v\n", err)
		os.Exit(1)
	}

	// Stop existing container
	exec.Command("docker", "stop", containerName).Run()
	exec.Command("docker", "rm", containerName).Run()

	// Run container
	fmt.Printf("Starting %s...\n", containerName)
	runCmd := exec.Command("docker", "run", "-d",
		"--name", containerName,
		"--network", "internal",
		"--restart", "unless-stopped",
		"-e", "PORT=5000",
		"-e", "ENV=production",
		imageName)
	out, err := runCmd.CombinedOutput()
	if err != nil {
		fmt.Fprintf(os.Stderr, "docker run failed: %s\n", strings.TrimSpace(string(out)))
		os.Exit(1)
	}

	id := strings.TrimSpace(string(out))
	if len(id) > 12 {
		id = id[:12]
	}
	fmt.Printf("Deployed %s (%s)\n", containerName, id)
}

func serviceControl(action string) {
	if len(os.Args) < 5 {
		fmt.Fprintf(os.Stderr, "usage: congo service %s <repo> <name>\n", action)
		os.Exit(1)
	}
	containerName := fmt.Sprintf("%s-%s", os.Args[3], os.Args[4])

	var cmd *exec.Cmd
	switch action {
	case "stop":
		cmd = exec.Command("docker", "stop", containerName)
	case "start":
		cmd = exec.Command("docker", "start", containerName)
	default:
		fmt.Fprintf(os.Stderr, "unknown action %s\n", action)
		os.Exit(1)
	}

	if err := cmd.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "%s failed: %v\n", action, err)
		os.Exit(1)
	}
	fmt.Printf("%s %s\n", action, containerName)
}

func serviceStatus() {
	if len(os.Args) < 5 {
		fmt.Fprintln(os.Stderr, "usage: congo service status <repo> <name>")
		os.Exit(1)
	}
	containerName := fmt.Sprintf("%s-%s", os.Args[3], os.Args[4])

	out, err := exec.Command("docker", "inspect", "--format",
		"{{.State.Status}} ({{.State.Health.Status}})", containerName).Output()
	if err != nil {
		fmt.Println("not found")
		os.Exit(1)
	}
	fmt.Print(strings.TrimSpace(string(out)))
}