286 lines
6.8 KiB
Go
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()
|
|
}
|
|
}
|