// Package harness exposes the testcontainers / Docker / image-build // scaffolding shared by the Runtime Manager service-local integration // suite under [`galaxy/rtmanager/integration`](..). // // Only `_test.go` files (and the harness itself) reference this // package; production code paths in `cmd/rtmanager` never import it. // The package therefore stays out of the production binary's import // graph, identical to the in-package `pgtest` and `integration/internal/harness` // patterns it mirrors. package harness import ( "context" "database/sql" "net/url" "os" "sync" "testing" "time" "galaxy/postgres" "galaxy/rtmanager/internal/adapters/postgres/migrations" testcontainers "github.com/testcontainers/testcontainers-go" tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) const ( pgImage = "postgres:16-alpine" pgSuperUser = "galaxy" pgSuperPassword = "galaxy" pgSuperDatabase = "galaxy_rtmanager_it" pgServiceRole = "rtmanagerservice" pgServicePassword = "rtmanagerservice" pgServiceSchema = "rtmanager" pgStartupTimeout = 90 * time.Second // pgOperationTimeout bounds the per-statement deadline used by every // pool the harness opens. Short enough to surface a runaway // integration test promptly, long enough to absorb laptop-grade I/O. pgOperationTimeout = 10 * time.Second ) // PostgresEnv carries the per-package PostgreSQL fixture. The container // is started lazily on the first EnsurePostgres call and torn down by // ShutdownPostgres at TestMain exit. type PostgresEnv struct { container *tcpostgres.PostgresContainer pool *sql.DB scopedDSN string } // Pool returns the harness-owned `*sql.DB` scoped to the rtmanager // schema. Tests use it to read durable state directly through the // existing store adapters. func (env *PostgresEnv) Pool() *sql.DB { return env.pool } // DSN returns the rtmanager-role-scoped DSN suitable for // `RTMANAGER_POSTGRES_PRIMARY_DSN`. Both this DSN and Pool address the // same database; the pool is reused across tests, while the runtime // under test opens its own pool through this DSN. func (env *PostgresEnv) DSN() string { return env.scopedDSN } var ( pgOnce sync.Once pgEnv *PostgresEnv pgErr error ) // EnsurePostgres starts the per-package PostgreSQL container on first // invocation and applies the embedded goose migrations. Subsequent // invocations reuse the same container. When Docker is unavailable the // helper calls `t.Skip` so the suite stays green on hosts without a // daemon (mirrors the contract from `internal/adapters/postgres/internal/pgtest`). func EnsurePostgres(t testing.TB) *PostgresEnv { t.Helper() pgOnce.Do(func() { pgEnv, pgErr = startPostgres() }) if pgErr != nil { t.Skipf("rtmanager integration: postgres container start failed (Docker unavailable?): %v", pgErr) } return pgEnv } // TruncatePostgres wipes every Runtime Manager table inside the shared // pool, leaving the schema and indexes intact. Tests call this from // their setup so each scenario starts on an empty state. func TruncatePostgres(t testing.TB) { t.Helper() env := EnsurePostgres(t) const stmt = `TRUNCATE TABLE runtime_records, operation_log, health_snapshots RESTART IDENTITY CASCADE` if _, err := env.pool.ExecContext(context.Background(), stmt); err != nil { t.Fatalf("truncate rtmanager tables: %v", err) } } // ShutdownPostgres terminates the shared container and closes the pool. // `TestMain` invokes it after `m.Run` so the container is released even // if individual tests panic. func ShutdownPostgres() { if pgEnv == nil { return } if pgEnv.pool != nil { _ = pgEnv.pool.Close() } if pgEnv.container != nil { _ = testcontainers.TerminateContainer(pgEnv.container) } pgEnv = nil } // RunMain is a convenience helper for the integration package // `TestMain`: it runs the suite, captures the exit code, tears every // shared container down, and exits. Wiring it through one helper keeps // `TestMain` to two lines and centralises ordering. func RunMain(m *testing.M) { code := m.Run() ShutdownRedis() ShutdownPostgres() ShutdownDocker() os.Exit(code) } func startPostgres() (*PostgresEnv, error) { ctx := context.Background() container, err := tcpostgres.Run(ctx, pgImage, tcpostgres.WithDatabase(pgSuperDatabase), tcpostgres.WithUsername(pgSuperUser), tcpostgres.WithPassword(pgSuperPassword), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(pgStartupTimeout), ), ) if err != nil { return nil, err } baseDSN, err := container.ConnectionString(ctx, "sslmode=disable") if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } if err := provisionRoleAndSchema(ctx, baseDSN); err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } scopedDSN, err := scopedDSNForRole(baseDSN) if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } cfg := postgres.DefaultConfig() cfg.PrimaryDSN = scopedDSN cfg.OperationTimeout = pgOperationTimeout pool, err := postgres.OpenPrimary(ctx, cfg) if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } if err := postgres.Ping(ctx, pool, pgOperationTimeout); err != nil { _ = pool.Close() _ = testcontainers.TerminateContainer(container) return nil, err } if err := postgres.RunMigrations(ctx, pool, migrations.FS(), "."); err != nil { _ = pool.Close() _ = testcontainers.TerminateContainer(container) return nil, err } return &PostgresEnv{ container: container, pool: pool, scopedDSN: scopedDSN, }, nil } func provisionRoleAndSchema(ctx context.Context, baseDSN string) error { cfg := postgres.DefaultConfig() cfg.PrimaryDSN = baseDSN cfg.OperationTimeout = pgOperationTimeout db, err := postgres.OpenPrimary(ctx, cfg) if err != nil { return err } defer func() { _ = db.Close() }() statements := []string{ `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'rtmanagerservice') THEN CREATE ROLE rtmanagerservice LOGIN PASSWORD 'rtmanagerservice'; END IF; END $$;`, `CREATE SCHEMA IF NOT EXISTS rtmanager AUTHORIZATION rtmanagerservice;`, `GRANT USAGE ON SCHEMA rtmanager TO rtmanagerservice;`, } for _, statement := range statements { if _, err := db.ExecContext(ctx, statement); err != nil { return err } } return nil } func scopedDSNForRole(baseDSN string) (string, error) { parsed, err := url.Parse(baseDSN) if err != nil { return "", err } values := url.Values{} values.Set("search_path", pgServiceSchema) values.Set("sslmode", "disable") scoped := url.URL{ Scheme: parsed.Scheme, User: url.UserPassword(pgServiceRole, pgServicePassword), Host: parsed.Host, Path: parsed.Path, RawQuery: values.Encode(), } return scoped.String(), nil }