diff --git a/Makefile b/Makefile index 4ad66d7981..1c4cffd570 100644 --- a/Makefile +++ b/Makefile @@ -103,6 +103,10 @@ else gotestsum --junitfile $(ARTIFACT_DIR)/integration-junit.xml ./test/integration/... -count 1 -p 1 endif +.PHONY: ci-integration +ci-integration: + ./scripts/integration.sh + images: $(DOCKER) build . diff --git a/scripts/integration.sh b/scripts/integration.sh new file mode 100755 index 0000000000..3b979f7b01 --- /dev/null +++ b/scripts/integration.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Run integration tests with automatic PostgreSQL setup. +# +# In a devcontainer (or CI with a sidecar Postgres), uses the existing +# PostgreSQL service. On a bare host, spins up a temporary Postgres +# container with Podman and tears it down on exit. +# +# Set SIPPY_INTEGRATION_DSN to skip container management and connect +# to an already-running Postgres instance directly. + +set -euo pipefail + +DOCKER="podman" +PSQL_CONTAINER="sippy-integration-test-postgresql" +PSQL_PORT="23434" + +EXIT_CODE=0 + +# --------------------------------------------------------------------------- +# If the caller already provided a DSN, just run the tests. +# --------------------------------------------------------------------------- +if [ -n "${SIPPY_INTEGRATION_DSN:-}" ]; then + echo "Using caller-provided SIPPY_INTEGRATION_DSN" + make integration + exit $? +fi + +# --------------------------------------------------------------------------- +# Devcontainer vs host setup +# --------------------------------------------------------------------------- +if [ -f /run/.containerenv ]; then + # Inside devcontainer: reuse the existing PostgreSQL service. + ADMIN_DSN="${SIPPY_DATABASE_DSN:-postgresql://postgres:password@sippy-postgres:5432/postgres}" + export SIPPY_INTEGRATION_DSN="$ADMIN_DSN" + echo "Detected devcontainer — using existing PostgreSQL" + echo " DSN: ${SIPPY_INTEGRATION_DSN%%@*}@***" + + clean_up() { + ARG=$? + [ $ARG -ne 0 ] && EXIT_CODE=$ARG + exit $EXIT_CODE + } + trap clean_up EXIT +else + # On host: start a temporary Postgres container. + clean_up() { + ARG=$? + [ $ARG -ne 0 ] && EXIT_CODE=$ARG + echo "Tearing down container $PSQL_CONTAINER" + $DOCKER stop -i $PSQL_CONTAINER 2>/dev/null || true + $DOCKER rm -i $PSQL_CONTAINER 2>/dev/null || true + exit $EXIT_CODE + } + trap clean_up EXIT + + echo "Cleaning up old integration test container if present" + $DOCKER stop -i $PSQL_CONTAINER 2>/dev/null || true + $DOCKER rm -i $PSQL_CONTAINER 2>/dev/null || true + + echo "Starting PostgreSQL container: $PSQL_CONTAINER" + $DOCKER run --name $PSQL_CONTAINER \ + -e POSTGRES_PASSWORD=password \ + -p $PSQL_PORT:5432 \ + -d quay.io/enterprisedb/postgresql + + echo "Waiting for PostgreSQL to start..." + TIMEOUT=30 + ELAPSED=0 + while [ $ELAPSED -lt $TIMEOUT ]; do + if $DOCKER exec $PSQL_CONTAINER pg_isready -U postgres > /dev/null 2>&1; then + echo "PostgreSQL is ready after ${ELAPSED}s" + break + fi + sleep 1 + ELAPSED=$((ELAPSED + 1)) + done + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout waiting for PostgreSQL to start" + exit 1 + fi + + export SIPPY_INTEGRATION_DSN="postgresql://postgres:password@localhost:$PSQL_PORT/postgres" + echo " DSN: ${SIPPY_INTEGRATION_DSN%%@*}@***" +fi + +make integration +EXIT_CODE=$? diff --git a/test/integration/jobs_test.go b/test/integration/jobs_test.go index 209bbfdfd7..c41e37faf4 100644 --- a/test/integration/jobs_test.go +++ b/test/integration/jobs_test.go @@ -31,9 +31,13 @@ func runTests(m *testing.M) int { ctx := context.Background() var err error - pgContainer, err = intutil.StartPostgresContainer(ctx) + if dsn := os.Getenv("SIPPY_INTEGRATION_DSN"); dsn != "" { + pgContainer, err = intutil.ConnectToExternalPostgres(ctx, dsn) + } else { + pgContainer, err = intutil.StartPostgresContainer(ctx) + } if err != nil { - panic("failed to start postgres container: " + err.Error()) + panic("failed to start postgres: " + err.Error()) } defer func() { if err := pgContainer.Terminate(ctx); err != nil { diff --git a/test/integration/util/testdb.go b/test/integration/util/testdb.go index 828e6404eb..3d71678cab 100644 --- a/test/integration/util/testdb.go +++ b/test/integration/util/testdb.go @@ -126,9 +126,26 @@ func StartPostgresContainer(ctx context.Context) (*PostgresContainer, error) { }, nil } -// Terminate stops and removes the container. +// ConnectToExternalPostgres connects to an existing Postgres instance and +// creates a template database with the integration schema applied. Use this +// in CI environments where a Postgres service is provided externally and +// testcontainers cannot be used (no container runtime available). +func ConnectToExternalPostgres(ctx context.Context, dsn string) (*PostgresContainer, error) { + if err := createTemplateDB(dsn); err != nil { + return nil, fmt.Errorf("creating template database: %w", err) + } + return &PostgresContainer{ + baseDSN: dsn, + }, nil +} + +// Terminate stops and removes the container. When using an external +// Postgres (container is nil), this is a no-op. func (pc *PostgresContainer) Terminate(ctx context.Context) error { - return pc.container.Terminate(ctx) + if pc.container != nil { + return pc.container.Terminate(ctx) + } + return nil } // createTemplateDB creates a fresh database, applies the integration schema, @@ -140,6 +157,10 @@ func createTemplateDB(baseDSN string) error { } defer adminDB.Close() + // Unmark as template first (required before dropping a template database), + // then drop if it exists. Ignore errors here because the database may not + // exist yet on a fresh Postgres instance. + _, _ = adminDB.Exec(fmt.Sprintf("ALTER DATABASE %s IS_TEMPLATE = false", templateDB)) if _, err := adminDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", templateDB)); err != nil { return fmt.Errorf("dropping old template: %w", err) }