Files
fn_registry/functions/pipelines/init_metabase/main.go
T
egutierrez 2e5bdacdcf feat: metabase_setup Python, fix list_databases, volumen Docker en init_metabase
Nueva función metabase_setup para setup inicial via API. Fix list_databases
que no extraía data del response wrapper. Pipeline init_metabase soporta
--mb-volumes para montar SQLite como volumen con fix de permisos automático.
Añadido .env a gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:20 +01:00

206 lines
6.0 KiB
Go

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 Postgres (default: docker named volume)")
mbVolumes := flag.String("mb-volumes", "", "Volumes adicionales para Metabase, separados por coma (ej: /host/path:/container/path,/otro:/dest)")
flag.Parse()
if *project == "" {
fmt.Fprintln(os.Stderr, "error: --project requerido")
flag.Usage()
os.Exit(1)
}
var extraVolumes []string
if *mbVolumes != "" {
extraVolumes = strings.Split(*mbVolumes, ",")
}
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume, extraVolumes)
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, mbExtraVolumes []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,
}
for _, v := range mbExtraVolumes {
mbArgs = append(mbArgs, "-v", strings.TrimSpace(v))
}
mbArgs = append(mbArgs, "metabase/metabase:latest")
mbID, err := dockerCmd(mbArgs...)
if err != nil {
return nil, fmt.Errorf("starting metabase: %w", err)
}
// 6. Fix permisos para volumes SQLite — Metabase corre como UID 2000
for _, v := range mbExtraVolumes {
parts := strings.SplitN(strings.TrimSpace(v), ":", 2)
if len(parts) < 2 {
continue
}
destPath := parts[1]
// Si es un archivo, fix el directorio padre; si es directorio, fix directo
dir := destPath
if strings.Contains(destPath, ".") {
// Probablemente un archivo, usar dirname
idx := strings.LastIndex(destPath, "/")
if idx > 0 {
dir = destPath[:idx]
}
}
fmt.Fprintf(os.Stderr, " Fijando permisos de %s para usuario metabase...\n", dir)
dockerCmd("exec", "-u", "root", mbName, "chown", "metabase:metabase", dir)
}
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)
}