diff --git a/functions/infra/deploy_app_remote.go b/functions/infra/deploy_app_remote.go new file mode 100644 index 00000000..7dcc8eb5 --- /dev/null +++ b/functions/infra/deploy_app_remote.go @@ -0,0 +1,46 @@ +package infra + +import ( + "fmt" + "os/exec" + "strings" +) + +// DeployAppRemote orquesta el deploy continuo de una app a un VPS remoto. +// Pasos: verificar SSH → build local → rsync → restart systemd → health check. +func DeployAppRemote(conn SSHConn, cfg DeployConfig) error { + // 1. Verificar conectividad SSH + if err := SSHCheck(conn); err != nil { + return fmt.Errorf("deploy_app_remote: ssh check: %w", err) + } + + // 2. Build local (si hay comando de build) + if cfg.BuildCmd != "" { + cmd := exec.Command("bash", "-c", cfg.BuildCmd) + cmd.Dir = cfg.LocalDir + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("deploy_app_remote: build failed: %s\n%s", err, strings.TrimSpace(string(out))) + } + } + + // 3. Subir binario compilado + if err := uploadAppFiles(conn, cfg); err != nil { + return fmt.Errorf("deploy_app_remote: upload: %w", err) + } + + // 4. Restart systemd service + if err := SystemdRestart(conn, cfg.AppName); err != nil { + return fmt.Errorf("deploy_app_remote: restart: %w", err) + } + + // 5. Health check (si está configurado) + if cfg.HealthPath != "" && cfg.Port > 0 { + url := fmt.Sprintf("http://%s:%d%s", conn.Host, cfg.Port, cfg.HealthPath) + if err := HealthCheckHTTP(url, 30, 2000); err != nil { + return fmt.Errorf("deploy_app_remote: health check: %w", err) + } + } + + return nil +} diff --git a/functions/infra/deploy_app_remote.md b/functions/infra/deploy_app_remote.md new file mode 100644 index 00000000..f49fdfde --- /dev/null +++ b/functions/infra/deploy_app_remote.md @@ -0,0 +1,47 @@ +--- +name: deploy_app_remote +kind: pipeline +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func DeployAppRemote(conn SSHConn, cfg DeployConfig) error" +description: "Orquesta el deploy continuo de una app a un VPS: verifica SSH, compila localmente, sube binario, reinicia systemd y hace health check." +tags: [deploy, vps, remote, ci, cd, pipeline, infra] +uses_functions: [ssh_check_go_infra, ssh_upload_go_infra, ssh_exec_go_infra, systemd_restart_go_infra, health_check_http_go_infra] +uses_types: [ssh_conn_go_infra, DeployConfig_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt, os/exec, strings] +params: + - name: conn + desc: "conexión SSH al VPS destino" + - name: cfg + desc: "configuración de deploy con nombre, rutas, build command, puerto y health path" +output: "nil si el deploy completo fue exitoso" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/deploy_app_remote.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "185.x.x.x", User: "root"} +cfg := DeployConfig{ + AppName: "dag_engine", + LocalDir: "apps/dag_engine", + RemoteDir: "/opt/apps/dag_engine", + BinaryName: "dag_engine", + BuildCmd: "CGO_ENABLED=0 GOOS=linux go build -o dag_engine .", + Port: 8080, + HealthPath: "/api/health", +} +err := DeployAppRemote(conn, cfg) +``` + +## Notas + +Pipeline de 5 pasos para deploy continuo (asume que el setup inicial ya se hizo con `setup_vps_app`). El build corre localmente en `LocalDir` con `bash -c BuildCmd`. Si `BuildCmd` está vacío, se salta el build y sube directamente el binario existente. diff --git a/functions/infra/deploy_config.go b/functions/infra/deploy_config.go new file mode 100644 index 00000000..d826dfc1 --- /dev/null +++ b/functions/infra/deploy_config.go @@ -0,0 +1,14 @@ +package infra + +// DeployConfig parametriza un deploy de app a un VPS remoto. +type DeployConfig struct { + AppName string // nombre de la app (usado para systemd unit y logging) + LocalDir string // directorio local de la app (ej: apps/dag_engine) + RemoteDir string // directorio destino en el VPS (ej: /opt/apps/dag_engine) + BinaryName string // nombre del binario compilado (ej: dag_engine) + BuildCmd string // comando de build (ej: CGO_ENABLED=0 GOOS=linux go build -o dag_engine .) + ServiceUser string // usuario del sistema para el servicio (vacío = sin crear) + Port int // puerto del servicio (0 si no expone HTTP) + HealthPath string // path del health check (ej: /api/health, vacío = sin check) + Env map[string]string // variables de entorno para el servicio +} diff --git a/functions/infra/setup_vps_app.go b/functions/infra/setup_vps_app.go new file mode 100644 index 00000000..3372b0f6 --- /dev/null +++ b/functions/infra/setup_vps_app.go @@ -0,0 +1,61 @@ +package infra + +import "fmt" + +// SetupVPSApp orquesta el setup inicial de una app en un VPS remoto. +// Pasos: verificar SSH → crear dirs/usuario → rsync código → generar unit → instalar systemd → health check. +func SetupVPSApp(conn SSHConn, cfg DeployConfig) error { + // 1. Verificar conectividad SSH + if err := SSHCheck(conn); err != nil { + return fmt.Errorf("setup_vps_app: ssh check: %w", err) + } + + // 2. Preparar directorios y usuario en el VPS + if err := VPSSetupApp(conn, cfg.AppName, cfg.RemoteDir, cfg.ServiceUser); err != nil { + return fmt.Errorf("setup_vps_app: vps setup: %w", err) + } + + // 3. Subir binario o archivos de la app + if err := uploadAppFiles(conn, cfg); err != nil { + return fmt.Errorf("setup_vps_app: upload: %w", err) + } + + // 4. Generar unit de systemd + execStart := fmt.Sprintf("%s/%s", cfg.RemoteDir, cfg.BinaryName) + unit := SystemdGenerateUnit(cfg.AppName, execStart, cfg.RemoteDir, cfg.ServiceUser, cfg.Env) + + // 5. Instalar y arrancar servicio + if err := SystemdInstall(conn, cfg.AppName, unit); err != nil { + return fmt.Errorf("setup_vps_app: systemd install: %w", err) + } + + // 6. Health check (si hay endpoint configurado) + if cfg.HealthPath != "" && cfg.Port > 0 { + url := fmt.Sprintf("http://%s:%d%s", conn.Host, cfg.Port, cfg.HealthPath) + if err := HealthCheckHTTP(url, 30, 2000); err != nil { + return fmt.Errorf("setup_vps_app: health check: %w", err) + } + } + + return nil +} + +// uploadAppFiles sube el binario compilado al VPS via SCP. +func uploadAppFiles(conn SSHConn, cfg DeployConfig) error { + localBinary := fmt.Sprintf("%s/%s", cfg.LocalDir, cfg.BinaryName) + remoteBinary := fmt.Sprintf("%s/%s", cfg.RemoteDir, cfg.BinaryName) + + if err := SSHUpload(conn, localBinary, remoteBinary); err != nil { + return err + } + + // Hacer ejecutable + _, stderr, code, err := SSHExec(conn, fmt.Sprintf("chmod +x %s", remoteBinary)) + if err != nil { + return err + } + if code != 0 { + return fmt.Errorf("chmod: %s", stderr) + } + return nil +} diff --git a/functions/infra/setup_vps_app.md b/functions/infra/setup_vps_app.md new file mode 100644 index 00000000..c8fe9f44 --- /dev/null +++ b/functions/infra/setup_vps_app.md @@ -0,0 +1,48 @@ +--- +name: setup_vps_app +kind: pipeline +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func SetupVPSApp(conn SSHConn, cfg DeployConfig) error" +description: "Orquesta el setup inicial de una app en un VPS remoto: verifica SSH, crea dirs y usuario, sube binario, instala systemd unit y hace health check." +tags: [deploy, vps, setup, systemd, ssh, pipeline, infra] +uses_functions: [ssh_check_go_infra, vps_setup_app_go_infra, ssh_upload_go_infra, ssh_exec_go_infra, systemd_generate_unit_go_infra, systemd_install_go_infra, health_check_http_go_infra] +uses_types: [ssh_conn_go_infra, DeployConfig_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt] +params: + - name: conn + desc: "conexión SSH al VPS destino" + - name: cfg + desc: "configuración de deploy con nombre, rutas, build, puerto, env vars" +output: "nil si el setup completo fue exitoso" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/setup_vps_app.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "185.x.x.x", User: "root"} +cfg := DeployConfig{ + AppName: "dag_engine", + LocalDir: "apps/dag_engine", + RemoteDir: "/opt/apps/dag_engine", + BinaryName: "dag_engine", + ServiceUser: "deploy", + Port: 8080, + HealthPath: "/api/health", + Env: map[string]string{"PORT": "8080"}, +} +err := SetupVPSApp(conn, cfg) +``` + +## Notas + +Pipeline de 6 pasos para primera instalación. Después del setup inicial, usar `deploy_app_remote` para deploys continuos (no regenera dirs ni systemd unit). El health check espera hasta 30 segundos con polling cada 2s. diff --git a/functions/infra/systemd_generate_unit.go b/functions/infra/systemd_generate_unit.go new file mode 100644 index 00000000..f07d3807 --- /dev/null +++ b/functions/infra/systemd_generate_unit.go @@ -0,0 +1,46 @@ +package infra + +import ( + "fmt" + "sort" + "strings" +) + +// SystemdGenerateUnit genera el texto de un archivo .service de systemd. +func SystemdGenerateUnit(name, execStart, workDir, user string, env map[string]string) string { + var b strings.Builder + + b.WriteString("[Unit]\n") + b.WriteString(fmt.Sprintf("Description=%s\n", name)) + b.WriteString("After=network.target\n\n") + + b.WriteString("[Service]\n") + b.WriteString("Type=simple\n") + b.WriteString(fmt.Sprintf("ExecStart=%s\n", execStart)) + if workDir != "" { + b.WriteString(fmt.Sprintf("WorkingDirectory=%s\n", workDir)) + } + if user != "" { + b.WriteString(fmt.Sprintf("User=%s\n", user)) + } + + // Environment vars en orden determinista + if len(env) > 0 { + keys := make([]string, 0, len(env)) + for k := range env { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + b.WriteString(fmt.Sprintf("Environment=%s=%s\n", k, env[k])) + } + } + + b.WriteString("Restart=on-failure\n") + b.WriteString("RestartSec=5\n\n") + + b.WriteString("[Install]\n") + b.WriteString("WantedBy=multi-user.target\n") + + return b.String() +} diff --git a/functions/infra/systemd_generate_unit.md b/functions/infra/systemd_generate_unit.md new file mode 100644 index 00000000..ae847379 --- /dev/null +++ b/functions/infra/systemd_generate_unit.md @@ -0,0 +1,50 @@ +--- +name: systemd_generate_unit +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: pure +signature: "func SystemdGenerateUnit(name, execStart, workDir, user string, env map[string]string) string" +description: "Genera el texto de un archivo .service de systemd para una app. Incluye restart automático y env vars en orden determinista." +tags: [systemd, unit, service, generate, deploy] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [fmt, sort, strings] +params: + - name: name + desc: "nombre del servicio (aparece en Description)" + - name: execStart + desc: "comando completo para arrancar la app (ruta absoluta al binario + args)" + - name: workDir + desc: "directorio de trabajo del servicio (vacío para omitir)" + - name: user + desc: "usuario del sistema bajo el que corre el servicio (vacío para omitir)" + - name: env + desc: "variables de entorno key=value para el servicio" +output: "texto completo del archivo .service listo para escribir a /etc/systemd/system/" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/systemd_generate_unit.go" +--- + +## Ejemplo + +```go +unit := SystemdGenerateUnit( + "dag_engine", + "/opt/apps/dag_engine/dag_engine", + "/opt/apps/dag_engine", + "deploy", + map[string]string{"PORT": "8080", "DB_PATH": "/opt/apps/dag_engine/data/ops.db"}, +) +fmt.Println(unit) +``` + +## Notas + +Función pura sin I/O. Las env vars se ordenan alfabéticamente para output determinista. Genera un unit con Restart=on-failure y RestartSec=5. diff --git a/functions/infra/systemd_install.go b/functions/infra/systemd_install.go new file mode 100644 index 00000000..bee3b6d0 --- /dev/null +++ b/functions/infra/systemd_install.go @@ -0,0 +1,34 @@ +package infra + +import "fmt" + +// SystemdInstall sube un unit file al host remoto, hace daemon-reload, enable y restart. +// Idempotente: si el unit ya existe, lo reemplaza. +func SystemdInstall(conn SSHConn, unitName, unitContent string) error { + // Escribir a archivo temporal y mover a /etc/systemd/system/ + tmpPath := fmt.Sprintf("/tmp/%s.service", unitName) + destPath := fmt.Sprintf("/etc/systemd/system/%s.service", unitName) + + writeCmd := fmt.Sprintf("cat > %s << 'UNIT_EOF'\n%sUNIT_EOF", tmpPath, unitContent) + _, stderr, code, err := SSHExec(conn, writeCmd) + if err != nil { + return fmt.Errorf("systemd_install: ssh write: %w", err) + } + if code != 0 { + return fmt.Errorf("systemd_install: write unit file: %s", stderr) + } + + // Mover a systemd y aplicar + cmds := fmt.Sprintf("sudo mv %s %s && sudo systemctl daemon-reload && sudo systemctl enable %s && sudo systemctl restart %s", + tmpPath, destPath, unitName, unitName) + + _, stderr, code, err = SSHExec(conn, cmds) + if err != nil { + return fmt.Errorf("systemd_install: ssh exec: %w", err) + } + if code != 0 { + return fmt.Errorf("systemd_install: %s", stderr) + } + + return nil +} diff --git a/functions/infra/systemd_install.md b/functions/infra/systemd_install.md new file mode 100644 index 00000000..bdfec4cf --- /dev/null +++ b/functions/infra/systemd_install.md @@ -0,0 +1,41 @@ +--- +name: systemd_install +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func SystemdInstall(conn SSHConn, unitName, unitContent string) error" +description: "Sube un unit file al host remoto, hace daemon-reload, enable y restart. Idempotente: reemplaza si el unit ya existe." +tags: [systemd, install, deploy, service, remote] +uses_functions: [ssh_exec_go_infra] +uses_types: [ssh_conn_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt] +params: + - name: conn + desc: "conexión SSH al host remoto" + - name: unitName + desc: "nombre del unit sin extensión (ej: dag_engine)" + - name: unitContent + desc: "contenido completo del archivo .service (generado por SystemdGenerateUnit)" +output: "nil si el unit se instaló y arrancó correctamente" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/systemd_install.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "192.168.1.100", User: "deploy"} +unit := SystemdGenerateUnit("dag_engine", "/opt/apps/dag_engine/dag_engine", "/opt/apps/dag_engine", "deploy", nil) +err := SystemdInstall(conn, "dag_engine", unit) +``` + +## Notas + +Escribe el unit a un archivo temporal en /tmp y lo mueve con sudo a /etc/systemd/system/. Requiere que el usuario SSH tenga permisos sudo sin password para systemctl y mv a /etc/systemd/system/. diff --git a/functions/infra/systemd_restart.go b/functions/infra/systemd_restart.go new file mode 100644 index 00000000..acc7d9de --- /dev/null +++ b/functions/infra/systemd_restart.go @@ -0,0 +1,15 @@ +package infra + +import "fmt" + +// SystemdRestart reinicia un servicio systemd en un host remoto. +func SystemdRestart(conn SSHConn, unitName string) error { + _, stderr, code, err := SSHExec(conn, fmt.Sprintf("sudo systemctl restart %s", unitName)) + if err != nil { + return fmt.Errorf("systemd_restart: ssh exec: %w", err) + } + if code != 0 { + return fmt.Errorf("systemd_restart %s: %s", unitName, stderr) + } + return nil +} diff --git a/functions/infra/systemd_restart.md b/functions/infra/systemd_restart.md new file mode 100644 index 00000000..6ce30e5d --- /dev/null +++ b/functions/infra/systemd_restart.md @@ -0,0 +1,40 @@ +--- +name: systemd_restart +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func SystemdRestart(conn SSHConn, unitName string) error" +description: "Reinicia un servicio systemd en un host remoto via SSH." +tags: [systemd, restart, service, remote] +uses_functions: [ssh_exec_go_infra] +uses_types: [ssh_conn_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt] +params: + - name: conn + desc: "conexión SSH al host remoto" + - name: unitName + desc: "nombre del unit systemd a reiniciar (sin .service)" +output: "nil si el reinicio fue exitoso" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/systemd_restart.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "192.168.1.100", User: "deploy"} +if err := SystemdRestart(conn, "dag_engine"); err != nil { + log.Fatal(err) +} +``` + +## Notas + +Usa `sudo systemctl restart`. Requiere que el usuario SSH tenga permisos sudo. Si el servicio no existe, systemctl retorna error. diff --git a/functions/infra/systemd_status.go b/functions/infra/systemd_status.go new file mode 100644 index 00000000..9fcf2c3c --- /dev/null +++ b/functions/infra/systemd_status.go @@ -0,0 +1,57 @@ +package infra + +import ( + "fmt" + "strings" +) + +// SystemdServiceStatus resultado de consultar el estado de un servicio systemd. +type SystemdServiceStatus struct { + Unit string + Active string // "active", "inactive", "failed" + SubState string // "running", "dead", "failed" + MainPID string + Logs string +} + +// SystemdStatus consulta el estado de un servicio systemd en un host remoto. +func SystemdStatus(conn SSHConn, unitName string, logLines int) (SystemdServiceStatus, error) { + status := SystemdServiceStatus{Unit: unitName} + + // Obtener propiedades del servicio + cmd := fmt.Sprintf("systemctl show %s --property=ActiveState,SubState,MainPID --no-pager", unitName) + stdout, stderr, code, err := SSHExec(conn, cmd) + if err != nil { + return status, fmt.Errorf("systemd_status: ssh exec: %w", err) + } + if code != 0 { + return status, fmt.Errorf("systemd_status: systemctl show: %s", stderr) + } + + // Parsear propiedades key=value + for _, line := range strings.Split(stdout, "\n") { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + switch parts[0] { + case "ActiveState": + status.Active = parts[1] + case "SubState": + status.SubState = parts[1] + case "MainPID": + status.MainPID = parts[1] + } + } + + // Obtener logs recientes + if logLines > 0 { + logCmd := fmt.Sprintf("journalctl -u %s -n %d --no-pager 2>/dev/null || true", unitName, logLines) + logOut, _, _, logErr := SSHExec(conn, logCmd) + if logErr == nil { + status.Logs = strings.TrimSpace(logOut) + } + } + + return status, nil +} diff --git a/functions/infra/systemd_status.md b/functions/infra/systemd_status.md new file mode 100644 index 00000000..5078196a --- /dev/null +++ b/functions/infra/systemd_status.md @@ -0,0 +1,45 @@ +--- +name: systemd_status +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func SystemdStatus(conn SSHConn, unitName string, logLines int) (SystemdServiceStatus, error)" +description: "Consulta el estado de un servicio systemd en un host remoto. Retorna estado activo, sub-estado, PID y logs recientes." +tags: [systemd, status, monitor, service, remote] +uses_functions: [ssh_exec_go_infra] +uses_types: [ssh_conn_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt, strings] +params: + - name: conn + desc: "conexión SSH al host remoto" + - name: unitName + desc: "nombre del unit systemd a consultar (sin .service)" + - name: logLines + desc: "número de líneas de journalctl a incluir (0 para no incluir logs)" +output: "SystemdServiceStatus con Active, SubState, MainPID y Logs del servicio" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/systemd_status.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "192.168.1.100", User: "deploy"} +s, err := SystemdStatus(conn, "dag_engine", 20) +if err != nil { + log.Fatal(err) +} +fmt.Printf("State: %s/%s PID: %s\n", s.Active, s.SubState, s.MainPID) +fmt.Println(s.Logs) +``` + +## Notas + +Usa `systemctl show` para obtener propiedades sin formato humano. Los logs se obtienen con journalctl y son opcionales (logLines=0 los omite). Si journalctl falla (ej: permisos), los logs quedan vacíos sin error. diff --git a/functions/infra/vps_setup_app.go b/functions/infra/vps_setup_app.go new file mode 100644 index 00000000..f3caead2 --- /dev/null +++ b/functions/infra/vps_setup_app.go @@ -0,0 +1,42 @@ +package infra + +import "fmt" + +// VPSSetupApp prepara un host remoto para recibir una app: +// crea directorios, usuario de servicio si no existe, y directorio de datos. +func VPSSetupApp(conn SSHConn, appName, remoteDir, serviceUser string) error { + // Crear directorio de la app y subdirectorios comunes + mkdirCmd := fmt.Sprintf("sudo mkdir -p %s/data %s/logs", remoteDir, remoteDir) + _, stderr, code, err := SSHExec(conn, mkdirCmd) + if err != nil { + return fmt.Errorf("vps_setup_app: ssh exec: %w", err) + } + if code != 0 { + return fmt.Errorf("vps_setup_app: mkdir: %s", stderr) + } + + // Crear usuario de servicio si se especificó y no existe + if serviceUser != "" { + userCmd := fmt.Sprintf("id %s >/dev/null 2>&1 || sudo useradd -r -s /usr/sbin/nologin -d %s %s", + serviceUser, remoteDir, serviceUser) + _, stderr, code, err = SSHExec(conn, userCmd) + if err != nil { + return fmt.Errorf("vps_setup_app: ssh exec: %w", err) + } + if code != 0 { + return fmt.Errorf("vps_setup_app: create user: %s", stderr) + } + + // Asignar ownership al usuario de servicio + chownCmd := fmt.Sprintf("sudo chown -R %s:%s %s", serviceUser, serviceUser, remoteDir) + _, stderr, code, err = SSHExec(conn, chownCmd) + if err != nil { + return fmt.Errorf("vps_setup_app: ssh exec: %w", err) + } + if code != 0 { + return fmt.Errorf("vps_setup_app: chown: %s", stderr) + } + } + + return nil +} diff --git a/functions/infra/vps_setup_app.md b/functions/infra/vps_setup_app.md new file mode 100644 index 00000000..d0a040e5 --- /dev/null +++ b/functions/infra/vps_setup_app.md @@ -0,0 +1,42 @@ +--- +name: vps_setup_app +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func VPSSetupApp(conn SSHConn, appName, remoteDir, serviceUser string) error" +description: "Prepara un host remoto para recibir una app: crea directorios, usuario de servicio y asigna ownership." +tags: [vps, setup, deploy, remote, infra] +uses_functions: [ssh_exec_go_infra] +uses_types: [ssh_conn_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt] +params: + - name: conn + desc: "conexión SSH al host remoto" + - name: appName + desc: "nombre de la app (para logging)" + - name: remoteDir + desc: "ruta absoluta donde vivirá la app en el remoto (ej: /opt/apps/dag_engine)" + - name: serviceUser + desc: "usuario del sistema para correr el servicio (vacío para omitir creación de usuario)" +output: "nil si el setup fue exitoso" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/vps_setup_app.go" +--- + +## Ejemplo + +```go +conn := SSHConn{Host: "192.168.1.100", User: "deploy"} +err := VPSSetupApp(conn, "dag_engine", "/opt/apps/dag_engine", "deploy") +``` + +## Notas + +Idempotente: mkdir -p no falla si el directorio existe, useradd se salta si el usuario existe. Crea subdirectorios `data/` y `logs/` dentro del remoteDir. Requiere sudo. diff --git a/types/infra/deploy_config.md b/types/infra/deploy_config.md new file mode 100644 index 00000000..9b73b0ac --- /dev/null +++ b/types/infra/deploy_config.md @@ -0,0 +1,32 @@ +--- +name: DeployConfig +lang: go +domain: infra +version: "1.0.0" +algebraic: product +definition: "type DeployConfig struct { AppName, LocalDir, RemoteDir, BinaryName, BuildCmd, ServiceUser string; Port int; HealthPath string; Env map[string]string }" +description: "Parametriza un deploy de app a un VPS remoto. Agrupa nombre, rutas, build, servicio, puerto, health check y env vars." +tags: [deploy, config, vps, remote, infra] +uses_types: [] +file_path: "functions/infra/deploy_config.go" +--- + +## Ejemplo + +```go +cfg := DeployConfig{ + AppName: "dag_engine", + LocalDir: "apps/dag_engine", + RemoteDir: "/opt/apps/dag_engine", + BinaryName: "dag_engine", + BuildCmd: "CGO_ENABLED=0 GOOS=linux go build -o dag_engine .", + ServiceUser: "deploy", + Port: 8080, + HealthPath: "/api/health", + Env: map[string]string{"DB_PATH": "/opt/apps/dag_engine/data/ops.db"}, +} +``` + +## Notas + +Usado por los pipelines `setup_vps_app` y `deploy_app_remote`. El campo `BuildCmd` se ejecuta localmente con `bash -c` en el directorio `LocalDir`. Si `HealthPath` está vacío o `Port` es 0, se omite el health check.