feat: pipeline init_metabase para desplegar Metabase + Postgres
Pipeline ejecutable que orquesta: crear red Docker, pull de imágenes, iniciar Postgres con volume persistente, health check con retry exponencial (pg_isready), e iniciar Metabase conectado via red interna. Configurable con flags: --project, --metabase-port, --pg-user, etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package pipelines
|
||||
|
||||
// InitMetabase despliega un stack Metabase + Postgres en Docker.
|
||||
//
|
||||
// Pasos:
|
||||
// 1. Crear red Docker compartida
|
||||
// 2. Pull de imágenes postgres:16 y metabase/metabase:latest
|
||||
// 3. Iniciar Postgres con volume persistente
|
||||
// 4. Esperar a que Postgres acepte conexiones (health check con retry)
|
||||
// 5. Iniciar Metabase conectado a Postgres
|
||||
//
|
||||
// Implementation: functions/pipelines/init_metabase/main.go
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: init_metabase
|
||||
kind: pipeline
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func main() — Despliega stack Metabase + Postgres en Docker"
|
||||
description: "Pipeline que inicializa un contenedor Metabase con su base de datos Postgres. Crea red Docker, pull de imágenes, inicia Postgres con volume persistente, espera health check y lanza Metabase conectado."
|
||||
tags: [docker, metabase, postgres, pipeline, infra, analytics]
|
||||
uses_functions:
|
||||
- docker_create_network_go_infra
|
||||
- docker_pull_image_go_infra
|
||||
- docker_run_container_go_infra
|
||||
- docker_inspect_container_go_infra
|
||||
- retry_with_backoff_go_core
|
||||
uses_types:
|
||||
- container_info_go_infra
|
||||
- network_go_docker
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os/exec, encoding/json]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/pipelines/init_metabase/main.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
go run functions/pipelines/init_metabase/main.go \
|
||||
--project analytics \
|
||||
--metabase-port 3000 \
|
||||
--pg-port 5432 \
|
||||
--pg-user metabase \
|
||||
--pg-password metabase \
|
||||
--pg-database metabase
|
||||
```
|
||||
|
||||
Salida JSON:
|
||||
```json
|
||||
{
|
||||
"network_id": "abc123...",
|
||||
"postgres_id": "def456...",
|
||||
"metabase_id": "ghi789...",
|
||||
"network_name": "analytics-net",
|
||||
"postgres_name": "analytics-postgres",
|
||||
"metabase_name": "analytics-metabase",
|
||||
"metabase_url": "http://localhost:3000"
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El pipeline orquesta 5 pasos secuenciales:
|
||||
|
||||
1. **Red Docker** — crea `{project}-net` con driver bridge
|
||||
2. **Pull** — descarga `postgres:16` y `metabase/metabase:latest`
|
||||
3. **Postgres** — inicia con volume persistente (named volume por defecto o bind mount con `--pg-volume`)
|
||||
4. **Health check** — retry exponencial (hasta ~34 min) con `pg_isready` dentro del contenedor
|
||||
5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable
|
||||
|
||||
Reutiliza conceptualmente `docker_create_network`, `docker_pull_image`, `docker_run_container`, `docker_inspect_container` y `retry_with_backoff`, reimplementadas inline por ser un ejecutable independiente.
|
||||
|
||||
Para destruir el stack:
|
||||
```bash
|
||||
docker stop analytics-metabase analytics-postgres
|
||||
docker rm analytics-metabase analytics-postgres
|
||||
docker network rm analytics-net
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MetabaseResult contiene los IDs y nombres de los recursos creados.
|
||||
type MetabaseResult struct {
|
||||
NetworkID string `json:"network_id"`
|
||||
PostgresID string `json:"postgres_id"`
|
||||
MetabaseID string `json:"metabase_id"`
|
||||
NetworkName string `json:"network_name"`
|
||||
PostgresName string `json:"postgres_name"`
|
||||
MetabaseName string `json:"metabase_name"`
|
||||
MetabaseURL string `json:"metabase_url"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
project := flag.String("project", "", "Prefijo para nombres de contenedores y red (requerido)")
|
||||
mbPort := flag.String("metabase-port", "3000", "Puerto host para Metabase")
|
||||
pgPort := flag.String("pg-port", "5432", "Puerto host para Postgres")
|
||||
pgUser := flag.String("pg-user", "metabase", "Usuario Postgres")
|
||||
pgPass := flag.String("pg-password", "metabase", "Password Postgres")
|
||||
pgDB := flag.String("pg-database", "metabase", "Base de datos Postgres")
|
||||
pgVolume := flag.String("pg-volume", "", "Path host para persistencia (default: docker named volume)")
|
||||
flag.Parse()
|
||||
|
||||
if *project == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: --project requerido")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(result)
|
||||
}
|
||||
|
||||
func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string) (*MetabaseResult, error) {
|
||||
networkName := project + "-net"
|
||||
pgName := project + "-postgres"
|
||||
mbName := project + "-metabase"
|
||||
|
||||
// 1. Crear red
|
||||
fmt.Fprintf(os.Stderr, "[1/5] Creando red %s...\n", networkName)
|
||||
netID, err := dockerCreateNetwork(networkName, "bridge")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating network: %w", err)
|
||||
}
|
||||
|
||||
// 2. Pull imágenes
|
||||
for i, img := range []string{"postgres:16", "metabase/metabase:latest"} {
|
||||
fmt.Fprintf(os.Stderr, "[2/5] Pulling %s (%d/2)...\n", img, i+1)
|
||||
if err := dockerPull(img); err != nil {
|
||||
return nil, fmt.Errorf("pulling %s: %w", img, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Iniciar Postgres
|
||||
fmt.Fprintf(os.Stderr, "[3/5] Iniciando Postgres %s...\n", pgName)
|
||||
vol := pgName + "-data:/var/lib/postgresql/data"
|
||||
if pgVolume != "" {
|
||||
vol = pgVolume + ":/var/lib/postgresql/data"
|
||||
}
|
||||
|
||||
pgArgs := []string{
|
||||
"run", "-d",
|
||||
"--name", pgName,
|
||||
"--network", networkName,
|
||||
"-p", pgPort + ":5432",
|
||||
"-e", "POSTGRES_USER=" + pgUser,
|
||||
"-e", "POSTGRES_PASSWORD=" + pgPass,
|
||||
"-e", "POSTGRES_DB=" + pgDB,
|
||||
"-v", vol,
|
||||
"postgres:16",
|
||||
}
|
||||
pgID, err := dockerCmd(pgArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting postgres: %w", err)
|
||||
}
|
||||
|
||||
// 4. Health check con retry exponencial
|
||||
fmt.Fprintf(os.Stderr, "[4/5] Esperando a que Postgres esté listo...\n")
|
||||
err = retryWithBackoff(func() error {
|
||||
out, execErr := dockerCmd("exec", pgName, "pg_isready", "-U", pgUser)
|
||||
if execErr != nil {
|
||||
return fmt.Errorf("pg_isready: %s", out)
|
||||
}
|
||||
return nil
|
||||
}, 10, 2*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("waiting for postgres: %w", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " Postgres listo.\n")
|
||||
|
||||
// 5. Iniciar Metabase
|
||||
fmt.Fprintf(os.Stderr, "[5/5] Iniciando Metabase %s...\n", mbName)
|
||||
mbArgs := []string{
|
||||
"run", "-d",
|
||||
"--name", mbName,
|
||||
"--network", networkName,
|
||||
"-p", mbPort + ":3000",
|
||||
"-e", "MB_DB_TYPE=postgres",
|
||||
"-e", "MB_DB_DBNAME=" + pgDB,
|
||||
"-e", "MB_DB_PORT=5432",
|
||||
"-e", "MB_DB_USER=" + pgUser,
|
||||
"-e", "MB_DB_PASS=" + pgPass,
|
||||
"-e", "MB_DB_HOST=" + pgName,
|
||||
"metabase/metabase:latest",
|
||||
}
|
||||
mbID, err := dockerCmd(mbArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting metabase: %w", err)
|
||||
}
|
||||
|
||||
mbURL := fmt.Sprintf("http://localhost:%s", mbPort)
|
||||
fmt.Fprintf(os.Stderr, "\nStack listo. Metabase disponible en %s\n", mbURL)
|
||||
|
||||
return &MetabaseResult{
|
||||
NetworkID: netID,
|
||||
PostgresID: pgID,
|
||||
MetabaseID: mbID,
|
||||
NetworkName: networkName,
|
||||
PostgresName: pgName,
|
||||
MetabaseName: mbName,
|
||||
MetabaseURL: mbURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- Funciones Docker (reimplementan las del registry sin imports internos) ---
|
||||
|
||||
func dockerCreateNetwork(name, driver string) (string, error) {
|
||||
return dockerCmd("network", "create", "--driver", driver, name)
|
||||
}
|
||||
|
||||
func dockerPull(image string) error {
|
||||
_, err := dockerCmd("pull", image)
|
||||
return err
|
||||
}
|
||||
|
||||
func dockerCmd(args ...string) (string, error) {
|
||||
out, err := exec.Command("docker", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("docker %s: %s", args[0], strings.TrimSpace(string(out)))
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// retryWithBackoff reimplementa RetryWithBackoff de core para funciones () error.
|
||||
func retryWithBackoff(fn func() error, maxRetries int, baseDelay time.Duration) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if err := fn(); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
lastErr = err
|
||||
}
|
||||
if attempt < maxRetries {
|
||||
delay := baseDelay * (1 << uint(attempt))
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("all %d retries exhausted: %w", maxRetries+1, lastErr)
|
||||
}
|
||||
Reference in New Issue
Block a user