feat: funciones Go de deploy — systemd, VPS setup, deploy remoto
Nuevas funciones infra para deploy sin Docker: generación de units systemd (pura), instalación/restart/status de servicios remotos via SSH, setup inicial de VPS (crear dirs, usuario, permisos), y pipelines de deploy completo (setup_vps_app, deploy_app_remote). Incluye tipo DeployConfig con la configuración de deploy por app.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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/.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user