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/)") 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/)") 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 ") 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 [--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 --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 | --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() } }