Files
deploy_server/commands.go
T
2026-04-28 22:12:20 +02:00

286 lines
6.8 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"text/tabwriter"
)
func registryRoot() string {
if r := os.Getenv("FN_REGISTRY_ROOT"); r != "" {
return r
}
// Subir dos niveles desde apps/deploy_server/
exe, _ := os.Executable()
return filepath.Dir(filepath.Dir(filepath.Dir(exe)))
}
func storeDir() string {
// operations.db en el directorio de la app
return "operations.db"
}
func openStoreOrDie() *Store {
s, err := OpenStore(storeDir())
if err != nil {
fmt.Fprintf(os.Stderr, "error opening store: %v\n", err)
os.Exit(1)
}
return s
}
// --- target ---
func cmdTarget(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: deploy_server target add|list|remove")
os.Exit(1)
}
switch args[0] {
case "add":
cmdTargetAdd(args[1:])
case "list":
cmdTargetList()
case "remove":
cmdTargetRemove(args[1:])
default:
fmt.Fprintf(os.Stderr, "unknown target subcommand: %s\n", args[0])
os.Exit(1)
}
}
func cmdTargetAdd(args []string) {
fs := flag.NewFlagSet("target add", flag.ExitOnError)
app := fs.String("app", "", "app name (matches apps/ directory or repo name)")
host := fs.String("host", "", "SSH alias from ~/.ssh/config")
remoteDir := fs.String("remote-dir", "", "remote directory (default: /opt/apps/<app>)")
binaryName := fs.String("binary", "", "binary name (default: app name for systemd strategies)")
buildCmd := fs.String("build", "", "build command (local for systemd, remote for systemd-remote)")
serviceUser := fs.String("user", "", "systemd service user")
port := fs.Int("port", 0, "service port")
healthPath := fs.String("health", "", "health check path (e.g. /api/health)")
envJSON := fs.String("env", "{}", "env vars as JSON object")
strategy := fs.String("strategy", "systemd", "deploy strategy: systemd, systemd-remote, docker-compose")
sourceDir := fs.String("source-dir", "", "local source dir relative to registry root (overrides apps/<app>)")
branch := fs.String("branch", "main", "git branch for remote deploys")
composeFiles := fs.String("compose-files", "", "comma-separated extra compose files for docker-compose strategy")
fs.Parse(args)
if *app == "" || *host == "" {
fmt.Fprintln(os.Stderr, "required: --app and --host")
fs.Usage()
os.Exit(1)
}
if *remoteDir == "" {
*remoteDir = "/opt/apps/" + *app
}
if *binaryName == "" && *strategy != "docker-compose" {
*binaryName = *app
}
var env map[string]string
json.Unmarshal([]byte(*envJSON), &env)
s := openStoreOrDie()
defer s.Close()
t := DeployTarget{
App: *app,
Host: *host,
RemoteDir: *remoteDir,
BinaryName: *binaryName,
BuildCmd: *buildCmd,
ServiceUser: *serviceUser,
Port: *port,
HealthPath: *healthPath,
Env: env,
Strategy: *strategy,
SourceDir: *sourceDir,
Branch: *branch,
ComposeFiles: *composeFiles,
}
if err := s.AddTarget(t); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("target added: %s [%s] → %s:%s\n", *app, *strategy, *host, *remoteDir)
}
func cmdTargetList() {
s := openStoreOrDie()
defer s.Close()
targets, err := s.ListTargets()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if len(targets) == 0 {
fmt.Println("No deploy targets configured.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "APP\tSTRATEGY\tHOST\tREMOTE DIR\tPORT\tHEALTH")
for _, t := range targets {
health := t.HealthPath
if health == "" {
health = "-"
}
strategy := t.Strategy
if strategy == "" {
strategy = "systemd"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%s\n", t.App, strategy, t.Host, t.RemoteDir, t.Port, health)
}
w.Flush()
}
func cmdTargetRemove(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: deploy_server target remove <app>")
os.Exit(1)
}
s := openStoreOrDie()
defer s.Close()
if err := s.RemoveTarget(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("target removed: %s\n", args[0])
}
// --- deploy ---
func cmdDeploy(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: deploy_server deploy <app> [--host HOST]")
os.Exit(1)
}
fs := flag.NewFlagSet("deploy", flag.ExitOnError)
host := fs.String("host", "", "deploy to specific host only")
fs.Parse(args[1:])
app := args[0]
s := openStoreOrDie()
defer s.Close()
targets, err := s.GetTargets(app)
if err != nil || len(targets) == 0 {
fmt.Fprintf(os.Stderr, "no deploy targets for %q\n", app)
os.Exit(1)
}
d := NewDeployer(s, registryRoot())
for _, t := range targets {
if *host != "" && t.Host != *host {
continue
}
fmt.Printf("deploying %s → %s:%s\n", t.App, t.Host, t.RemoteDir)
if err := d.Deploy(t, "manual"); err != nil {
fmt.Fprintf(os.Stderr, " FAILED: %v\n", err)
} else {
fmt.Printf(" OK\n")
}
}
}
// --- setup ---
func cmdSetup(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: deploy_server setup <app> --host HOST")
os.Exit(1)
}
fs := flag.NewFlagSet("setup", flag.ExitOnError)
host := fs.String("host", "", "SSH host alias (required)")
fs.Parse(args[1:])
if *host == "" {
fmt.Fprintln(os.Stderr, "required: --host")
os.Exit(1)
}
app := args[0]
s := openStoreOrDie()
defer s.Close()
targets, err := s.GetTargets(app)
if err != nil || len(targets) == 0 {
fmt.Fprintf(os.Stderr, "no deploy target for %q — add one first with 'target add'\n", app)
os.Exit(1)
}
d := NewDeployer(s, registryRoot())
for _, t := range targets {
if t.Host != *host {
continue
}
fmt.Printf("setting up %s on %s...\n", t.App, t.Host)
if err := d.Setup(t); err != nil {
fmt.Fprintf(os.Stderr, "FAILED: %v\n", err)
os.Exit(1)
}
return
}
fmt.Fprintf(os.Stderr, "no target for %s on host %s\n", app, *host)
os.Exit(1)
}
// --- status ---
func cmdStatus(args []string) {
fs := flag.NewFlagSet("status", flag.ExitOnError)
all := fs.Bool("all", false, "show all targets")
fs.Parse(args)
s := openStoreOrDie()
defer s.Close()
d := NewDeployer(s, registryRoot())
var targets []DeployTarget
if *all {
targets, _ = s.ListTargets()
} else if fs.NArg() > 0 {
targets, _ = s.GetTargets(fs.Arg(0))
} else {
fmt.Fprintln(os.Stderr, "usage: deploy_server status <app> | --all")
os.Exit(1)
}
for _, t := range targets {
fmt.Printf("=== %s @ %s ===\n", t.App, t.Host)
out, err := d.Status(t)
if err != nil {
fmt.Fprintf(os.Stderr, " error: %v\n", err)
} else {
fmt.Println(out)
}
// Últimos 3 deploys
logs, _ := s.RecentLogs(t.App, 3)
if len(logs) > 0 {
fmt.Println(" Recent deploys:")
for _, l := range logs {
errStr := ""
if l.Error != "" {
errStr = " — " + l.Error
}
fmt.Printf(" %s %s %s %dms%s\n", l.StartedAt.Format("2006-01-02 15:04"), l.Trigger, l.Status, l.Duration, errStr)
}
}
fmt.Println()
}
}