diff --git a/main.go b/main.go index 22272a5..23d4220 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ func env(key, fallback string) string { return fallback } -func testPostgres() result { +func openDB() (*sql.DB, error) { dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", env("POSTGRES_HOST", "localhost"), env("POSTGRES_PORT", "5432"), @@ -35,16 +35,51 @@ func testPostgres() result { env("POSTGRES_DB", "app"), ) db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + db.SetConnMaxLifetime(5 * time.Second) + return db, nil +} + +// migrate runs database migrations. +func migrate() error { + db, err := openDB() + if err != nil { + return fmt.Errorf("open db: %w", err) + } + defer db.Close() + + migrations := []string{ + `CREATE TABLE IF NOT EXISTS healthcheck ( + id SERIAL PRIMARY KEY, + checked_at TIMESTAMPTZ DEFAULT NOW(), + msg TEXT + )`, + `CREATE TABLE IF NOT EXISTS visits ( + id SERIAL PRIMARY KEY, + path TEXT NOT NULL, + visited_at TIMESTAMPTZ DEFAULT NOW() + )`, + } + + for i, m := range migrations { + if _, err := db.Exec(m); err != nil { + return fmt.Errorf("migration %d: %w", i, err) + } + log.Printf("migration %d: OK", i) + } + + log.Println("all migrations completed") + return nil +} + +func testPostgres() result { + db, err := openDB() if err != nil { return result{"PostgreSQL", false, fmt.Sprintf("open: %v", err)} } defer db.Close() - db.SetConnMaxLifetime(5 * time.Second) - - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS healthcheck (id SERIAL PRIMARY KEY, checked_at TIMESTAMPTZ DEFAULT NOW(), msg TEXT)`) - if err != nil { - return result{"PostgreSQL", false, fmt.Sprintf("create table: %v", err)} - } _, err = db.Exec(`INSERT INTO healthcheck (msg) VALUES ($1)`, fmt.Sprintf("ping at %s", time.Now().Format(time.RFC3339))) if err != nil { @@ -57,7 +92,10 @@ func testPostgres() result { return result{"PostgreSQL", false, fmt.Sprintf("count: %v", err)} } - return result{"PostgreSQL", true, fmt.Sprintf("OK — %d rows in healthcheck", count)} + var visits int + db.QueryRow(`SELECT COUNT(*) FROM visits`).Scan(&visits) + + return result{"PostgreSQL", true, fmt.Sprintf("OK — %d healthchecks, %d visits", count, visits)} } func testRedis() result { @@ -90,6 +128,12 @@ func handler(w http.ResponseWriter, r *http.Request) { return } + // Track visit + if db, err := openDB(); err == nil { + db.Exec(`INSERT INTO visits (path) VALUES ($1)`, r.URL.Path) + db.Close() + } + pg := testPostgres() rd := testRedis() @@ -109,10 +153,11 @@ func handler(w http.ResponseWriter, r *http.Request) { .ok .status { color: #4ade80; } .fail .status { color: #f87171; } .time { color: #6b7280; font-size: 12px; margin-top: 24px; } + .badge { display: inline-block; background: #1e3a5f; color: #60a5fa; font-size: 11px; padding: 2px 8px; border-radius: 4px; margin-left: 8px; } -

StackOps Test App

+

StackOps Test App with migrations

Dependency connectivity checks

`) for _, t := range []result{pg, rd} { @@ -132,6 +177,13 @@ func handler(w http.ResponseWriter, r *http.Request) { } func main() { + if len(os.Args) > 1 && os.Args[1] == "migrate" { + if err := migrate(); err != nil { + log.Fatalf("migration failed: %v", err) + } + return + } + http.HandleFunc("/", handler) port := env("PORT", "8080") log.Printf("test-app listening on :%s", port) diff --git a/stackfile.toml b/stackfile.toml index f04d7a4..abb381e 100644 --- a/stackfile.toml +++ b/stackfile.toml @@ -4,6 +4,9 @@ image = "git.nodeup.ru/stackops/test-app:latest" replicas = 1 port = 8080 +[migrations] +command = "test-app migrate" + [ingress] host = "test.app.nodeup.ru" path = "/"