github.com/quay/claircore@v1.5.28/test/integration/db.go (about) 1 package integration 2 3 import ( 4 "context" 5 crand "crypto/rand" 6 "encoding/binary" 7 "errors" 8 "fmt" 9 "io" 10 "math/rand" 11 "os" 12 "sync" 13 "testing" 14 15 "github.com/jackc/pgconn" 16 "github.com/jackc/pgx/v4" 17 "github.com/jackc/pgx/v4/log/testingadapter" 18 "github.com/jackc/pgx/v4/pgxpool" 19 ) 20 21 var ( 22 rngMu sync.Mutex 23 rng *rand.Rand 24 25 up sync.Once 26 pkgDB *Engine 27 pkgConfig *pgxpool.Config 28 ) 29 30 func mkIDs() (uint64, uint64) { 31 rngMu.Lock() 32 defer rngMu.Unlock() 33 return rng.Uint64(), rng.Uint64() 34 } 35 36 func init() { 37 // Seed our rng. 38 b := make([]byte, 8) 39 if _, err := io.ReadFull(crand.Reader, b); err != nil { 40 panic(err) 41 } 42 seed := rand.NewSource(int64(binary.BigEndian.Uint64(b))) 43 rng = rand.New(seed) 44 } 45 46 const ( 47 // EnvPGConnString is the environment variable examined for a DSN for a 48 // pre-existing database engine. If unset, an appropriate database will 49 // attempt to be downloaded and run. 50 EnvPGConnString = "POSTGRES_CONNECTION_STRING" 51 52 // EnvPGVersion is the environment variable examined for the version of 53 // PostgreSQL used if an embedded binary would be used. 54 EnvPGVersion = `PGVERSION` 55 56 loadUUID = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";` 57 createRole = `CREATE ROLE %s LOGIN PASSWORD '%[1]s';` 58 createDatabase = `CREATE DATABASE %[2]s WITH OWNER %[1]s ENCODING 'UTF8';` 59 killConnections = `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1` 60 dropDatabase = `DROP DATABASE %s;` 61 dropRole = `DROP ROLE %s;` 62 ) 63 64 // NewDB generates a unique database for use in integration tests. 65 // 66 // The returned database has a random name and a dedicated owner role 67 // configured. The "uuid-ossp" extension is already loaded. 68 // 69 // DBSetup and NeedDB are expected to have been called correctly. 70 func NewDB(ctx context.Context, t testing.TB) (*DB, error) { 71 dbid, roleid := mkIDs() 72 database := fmt.Sprintf("db%x", dbid) 73 role := fmt.Sprintf("role%x", roleid) 74 cfg := configureDatabase(ctx, t, pkgConfig, database, role) 75 if t.Failed() { 76 return nil, fmt.Errorf("failed to create database") 77 } 78 t.Logf("config: %+v", struct { 79 Host string 80 Database string 81 User string 82 Password string 83 Port uint16 84 }{ 85 Host: cfg.ConnConfig.Host, 86 Port: cfg.ConnConfig.Port, 87 Database: cfg.ConnConfig.Database, 88 User: cfg.ConnConfig.User, 89 Password: cfg.ConnConfig.Password, 90 }) 91 92 return &DB{ 93 cfg: cfg, 94 }, nil 95 } 96 97 func configureDatabase(ctx context.Context, t testing.TB, root *pgxpool.Config, database, role string) *pgxpool.Config { 98 var cfg *pgxpool.Config 99 // First, connect as the superuser to create the new database and role. 100 cfg = root.Copy() 101 cfg.ConnConfig.Logger = testingadapter.NewLogger(t) 102 cfg.MaxConns = 10 103 conn, err := pgx.ConnectConfig(ctx, cfg.ConnConfig) 104 if err != nil { 105 t.Error(err) 106 return nil 107 } 108 // The creation commands don't have "IF NOT EXISTS" forms, so check for the 109 // specific error codes that mean they already exist. 110 var pgErr *pgconn.PgError 111 _, err = conn.Exec(ctx, fmt.Sprintf(createRole, role)) 112 switch { 113 case errors.Is(err, nil): 114 case errors.As(err, &pgErr): 115 if pgErr.Code == "42710" { 116 t.Log("expected error:", pgErr.Message) 117 break 118 } 119 fallthrough 120 default: 121 t.Error(err) 122 } 123 _, err = conn.Exec(ctx, fmt.Sprintf(createDatabase, role, database)) 124 switch { 125 case errors.Is(err, nil): 126 case errors.As(err, &pgErr): 127 if pgErr.Code == "42P04" { 128 t.Log("expected error:", pgErr.Message) 129 break 130 } 131 fallthrough 132 default: 133 t.Error(err) 134 } 135 if err := conn.Close(ctx); err != nil { 136 t.Error(err) 137 } 138 if t.Failed() { 139 return nil 140 } 141 142 // Next, connect to the newly created database as the superuser to load the 143 // uuid extension 144 cfg = cfg.Copy() 145 cfg.ConnConfig.Database = database 146 conn, err = pgx.ConnectConfig(ctx, cfg.ConnConfig) 147 if err != nil { 148 t.Error(err) 149 return nil 150 } 151 if _, err := conn.Exec(ctx, loadUUID); err != nil { 152 t.Error(err) 153 } 154 if err := conn.Close(ctx); err != nil { 155 t.Error(err) 156 } 157 if t.Failed() { 158 return nil 159 } 160 161 // Finally, return a config setup to connect as the new role to the new 162 // database. 163 cfg = root.Copy() 164 cfg.ConnConfig.User = role 165 cfg.ConnConfig.Password = role 166 cfg.ConnConfig.Database = database 167 168 return cfg 169 } 170 171 // DB is a handle for connecting to and cleaning up a test database. 172 // 173 // If [testing.Verbose] reports true, the database engine will be run with the 174 // "auto_explain" module enabled. See the [auto_explain documentation] for more 175 // information. Setting the environment variable "PGEXPLAIN_FORMAT" will control 176 // the output format. This does not apply when the test harness is not 177 // controlling the database. 178 // 179 // [auto_explain documentation]: https://www.postgresql.org/docs/current/auto-explain.html 180 type DB struct { 181 cfg *pgxpool.Config 182 noDrop bool 183 } 184 185 func (db *DB) String() string { 186 const dsnFmt = `host=%s port=%d database=%s user=%s password=%s sslmode=disable` 187 return fmt.Sprintf(dsnFmt, 188 db.cfg.ConnConfig.Host, 189 db.cfg.ConnConfig.Port, 190 db.cfg.ConnConfig.Database, 191 db.cfg.ConnConfig.User, 192 db.cfg.ConnConfig.Password) 193 } 194 195 // Config returns a pgxpool.Config for the test database. 196 func (db *DB) Config() *pgxpool.Config { 197 return db.cfg.Copy() 198 } 199 200 // Close tears down the created database. 201 func (db *DB) Close(ctx context.Context, t testing.TB) { 202 cfg := pkgConfig.Copy() 203 cfg.ConnConfig.Logger = testingadapter.NewLogger(t) 204 conn, err := pgx.ConnectConfig(ctx, cfg.ConnConfig) 205 if err != nil { 206 t.Fatal(err) 207 } 208 defer conn.Close(ctx) 209 210 if _, err := conn.Exec(ctx, killConnections, db.cfg.ConnConfig.Database); err != nil { 211 t.Error(err) 212 } 213 214 if !db.noDrop { 215 if _, err := conn.Exec(ctx, fmt.Sprintf(dropDatabase, db.cfg.ConnConfig.Database)); err != nil { 216 t.Error(err) 217 } 218 if _, err := conn.Exec(ctx, fmt.Sprintf(dropRole, db.cfg.ConnConfig.User)); err != nil { 219 t.Error(err) 220 } 221 } 222 db.cfg = nil 223 } 224 225 // NeedDB is like Skip, except that the test will run if the needed binaries 226 // have been fetched. 227 // 228 // See the example for usage. 229 func NeedDB(t testing.TB) { 230 t.Helper() 231 if testing.Short() { 232 t.Skip(`skipping integration test: short tests`) 233 } 234 if inGHA { 235 up.Do(startGithubActions(t)) 236 return 237 } 238 if externalDB { 239 t.Log("using preconfigured external database") 240 up.Do(func() { 241 cfg, err := pgxpool.ParseConfig(os.Getenv(EnvPGConnString)) 242 if err != nil { 243 t.Fatal(err) 244 } 245 pkgConfig = cfg 246 // Database was started externally, we don't have to arrange to have it 247 // torn down. 248 }) 249 return 250 } 251 252 t.Log("using embedded database") 253 embedDB.DiscoverVersion(t) 254 up.Do(startEmbedded(t)) 255 } 256 257 // DBSetup queues setup and teardown for a postgres engine instance. If the 258 // "integration" build tag is not provided, then nothing is done. If the 259 // environment variable at EnvPGConnString is populated and the "integration" 260 // build tag is provided, then the value of that environment variable is used 261 // instead of an embedded postgres binary. 262 // 263 // See the example for usage. 264 func DBSetup() func() { 265 // This used to do a bunch of setup, but that got pretty gnarly. 266 // 267 // Most of the complexity was moved into [NeedDB] and helper functions run 268 // under the [up] sync.Once, because there's a [testing.T] that can actually 269 // log things. 270 return func() { 271 if pkgDB != nil { 272 pkgDB.Stop() 273 } 274 } 275 }