github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/storage/tests_utils.go (about) 1 package storage 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "os/exec" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/filters" 19 "github.com/docker/docker/api/types/network" 20 "github.com/docker/docker/client" 21 "github.com/docker/go-connections/nat" 22 "github.com/gocraft/dbr" 23 "github.com/kyma-project/kyma-environment-broker/internal/storage/postsql" 24 v1 "github.com/opencontainers/image-spec/specs-go/v1" 25 "github.com/sirupsen/logrus" 26 "github.com/stretchr/testify/assert" 27 "k8s.io/apimachinery/pkg/util/wait" 28 ) 29 30 const ( 31 DbUser = "admin" 32 DbPass = "nimda" 33 DbName = "broker" 34 DbPort = "5432" 35 DockerUserNetwork = "test_network" 36 EnvPipelineBuild = "PIPELINE_BUILD" 37 ) 38 39 var mappedPort string 40 41 func makeConnectionString(hostname string, port string) Config { 42 host := "localhost" 43 if os.Getenv(EnvPipelineBuild) != "" { 44 host = hostname 45 port = DbPort 46 } 47 48 cfg := Config{ 49 Host: host, 50 User: DbUser, 51 Password: DbPass, 52 Port: port, 53 Name: DbName, 54 SSLMode: "disable", 55 SecretKey: "$C&F)H@McQfTjWnZr4u7x!A%D*G-KaNd", 56 57 MaxOpenConns: 2, 58 MaxIdleConns: 1, 59 ConnMaxLifetime: time.Minute, 60 } 61 return cfg 62 } 63 64 func CloseDatabase(t *testing.T, connection *dbr.Connection) { 65 if connection != nil { 66 err := connection.Close() 67 assert.Nil(t, err, "Failed to close db connection") 68 } 69 } 70 71 func closeDBConnection(connection *dbr.Connection) { 72 if connection != nil { 73 err := connection.Close() 74 if err != nil { 75 log.Printf("failed to close db connection: %v", err) 76 } 77 } 78 } 79 80 func InitTestDBContainer(log func(format string, args ...interface{}), ctx context.Context, hostname string) (func(), Config, error) { 81 _, err := isDockerTestNetworkPresent(ctx) 82 if err != nil { 83 return nil, Config{}, fmt.Errorf("while testing docker network: %w", err) 84 } 85 86 isAvailable, dbCfg, err := isDBContainerAvailable(hostname, mappedPort) 87 if err != nil { 88 return nil, Config{}, fmt.Errorf("while checking db container: %w", err) 89 } else if !isAvailable { 90 log("cannot connect to DB container. Creating new Postgres container...") 91 } else if isAvailable { 92 return func() {}, dbCfg, nil 93 } 94 95 return createDbContainer(log, hostname) 96 } 97 98 func InitTestDBTables(t *testing.T, connectionURL string) (func(), error) { 99 connection, err := postsql.WaitForDatabaseAccess(connectionURL, 10, 1000*time.Millisecond, logrus.New()) 100 if err != nil { 101 t.Logf("Cannot connect to database with URL - reload test 2 - %s", connectionURL) 102 return nil, fmt.Errorf("while waiting for database access: %w", err) 103 } 104 105 cleanupFunc := func() { 106 _, err = connection.Exec(clearDBQuery()) 107 if err != nil { 108 err = fmt.Errorf("failed to clear DB tables: %w", err) 109 } 110 } 111 112 initialized, err := postsql.CheckIfDatabaseInitialized(connection) 113 if err != nil { 114 CloseDatabase(t, connection) 115 return nil, fmt.Errorf("while checking DB initialization: %w", err) 116 } else if initialized { 117 return cleanupFunc, nil 118 } 119 120 dirPath := "./../../../../migrations/" 121 files, err := ioutil.ReadDir(dirPath) 122 if err != nil { 123 log.Printf("Cannot read files from directory %s", dirPath) 124 return nil, fmt.Errorf("while reading migration data: %w", err) 125 } 126 127 for _, file := range files { 128 if strings.HasSuffix(file.Name(), "up.sql") { 129 v, err := ioutil.ReadFile(dirPath + file.Name()) 130 if err != nil { 131 log.Printf("Cannot read file %s", file.Name()) 132 } 133 if _, err = connection.Exec(string(v)); err != nil { 134 log.Printf("Cannot apply file %s", file.Name()) 135 return nil, fmt.Errorf("while applying migration files: %w", err) 136 } 137 } 138 } 139 log.Printf("Files applied to database") 140 141 return cleanupFunc, nil 142 } 143 144 func SetupTestDBTables(connectionURL string) (cleanupFunc func(), err error) { 145 connection, _ := postsql.WaitForDatabaseAccess(connectionURL, 10, 100*time.Millisecond, logrus.New()) 146 //if err != nil { 147 // log.Printf("Cannot connect to database with URL - reload test 3 - %s", connectionURL) 148 // return nil, fmt.Errorf("while waiting for database access: %w", err) 149 //} 150 return nil, nil 151 152 cleanupFunc = func() { 153 _, err = connection.Exec(clearDBQuery()) 154 if err != nil { 155 err = fmt.Errorf("failed to clear DB tables: %w", err) 156 } 157 } 158 159 initialized, err := postsql.CheckIfDatabaseInitialized(connection) 160 if err != nil { 161 closeDBConnection(connection) 162 return nil, fmt.Errorf("while checking DB initialization: %w", err) 163 } else if initialized { 164 return cleanupFunc, nil 165 } 166 167 dirPath := "./../../../../migrations/" 168 files, err := ioutil.ReadDir(dirPath) 169 if err != nil { 170 log.Printf("Cannot read files from directory %s", dirPath) 171 return nil, fmt.Errorf("while reading files from directory: %w", err) 172 } 173 174 for _, file := range files { 175 if strings.HasSuffix(file.Name(), "up.sql") { 176 v, err := ioutil.ReadFile(dirPath + file.Name()) 177 if err != nil { 178 log.Printf("Cannot read file %s", file.Name()) 179 } 180 if _, err = connection.Exec(string(v)); err != nil { 181 log.Printf("Cannot apply file %s", file.Name()) 182 return nil, fmt.Errorf("while executing migration: %w", err) 183 } 184 } 185 } 186 log.Printf("Files applied to database") 187 188 return cleanupFunc, nil 189 } 190 191 func dockerClient() (*client.Client, error) { 192 return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 193 } 194 195 func isDockerTestNetworkPresent(ctx context.Context) (bool, error) { 196 197 cli, err := dockerClient() 198 if err != nil { 199 return false, fmt.Errorf("while creating docker client: %w", err) 200 } 201 202 filterBy := filters.NewArgs() 203 filterBy.Add("name", DockerUserNetwork) 204 filterBy.Add("driver", "bridge") 205 list, err := cli.NetworkList(context.Background(), types.NetworkListOptions{Filters: filterBy}) 206 207 if err == nil { 208 return len(list) == 1, nil 209 } 210 211 return false, fmt.Errorf("while testing network availbility: %w", err) 212 } 213 214 func createTestNetworkForDB(ctx context.Context) (*types.NetworkResource, error) { 215 cli, err := dockerClient() 216 if err != nil { 217 return nil, fmt.Errorf("failed to create a Docker client: %w", err) 218 } 219 220 createdNetworkResponse, err := cli.NetworkCreate(context.Background(), DockerUserNetwork, types.NetworkCreate{Driver: "bridge"}) 221 if err != nil { 222 return nil, fmt.Errorf("failed to create docker user network: %w", err) 223 } 224 225 filterBy := filters.NewArgs() 226 filterBy.Add("id", createdNetworkResponse.ID) 227 list, err := cli.NetworkList(context.Background(), types.NetworkListOptions{Filters: filterBy}) 228 229 if err != nil || len(list) != 1 { 230 return nil, fmt.Errorf("network not found or not created: %w", err) 231 } 232 233 return &list[0], nil 234 } 235 236 func EnsureTestNetworkForDB(t *testing.T, ctx context.Context) (func(), error) { 237 exec.Command("systemctl start docker.service") 238 239 networkPresent, err := isDockerTestNetworkPresent(ctx) 240 if networkPresent && err == nil { 241 return func() {}, nil 242 } 243 244 if os.Getenv(EnvPipelineBuild) != "" { 245 return func() {}, fmt.Errorf("Docker network %s does not exist", DockerUserNetwork) 246 } 247 248 createdNetwork, err := createTestNetworkForDB(ctx) 249 250 if err != nil { 251 return func() {}, fmt.Errorf("while creating test network: %w", err) 252 } 253 254 cli, err := dockerClient() 255 if err != nil { 256 return nil, fmt.Errorf("failed to create a Docker client: %w", err) 257 } 258 259 cleanupFunc := func() { 260 err = cli.NetworkRemove(ctx, createdNetwork.ID) 261 assert.NoError(t, err) 262 time.Sleep(1 * time.Second) 263 } 264 265 return cleanupFunc, nil 266 } 267 268 func SetupTestNetworkForDB(ctx context.Context) (cleanupFunc func(), err error) { 269 exec.Command("systemctl start docker.service") 270 271 networkPresent, err := isDockerTestNetworkPresent(ctx) 272 if networkPresent && err == nil { 273 return func() {}, nil 274 } 275 276 createdNetwork, err := createTestNetworkForDB(ctx) 277 278 if err != nil { 279 return func() {}, fmt.Errorf("while creating test network: %w", err) 280 } 281 282 cli, err := dockerClient() 283 if err != nil { 284 return nil, fmt.Errorf("failed to create a Docker client: %w", err) 285 } 286 cleanupFunc = func() { 287 err = cli.NetworkRemove(ctx, createdNetwork.ID) 288 if err != nil { 289 err = fmt.Errorf("failed to remove docker network: %w + %s", err, DockerUserNetwork) 290 } 291 time.Sleep(1 * time.Second) 292 } 293 294 if err != nil { 295 return cleanupFunc, fmt.Errorf("while DB setup: %w", err) 296 } else { 297 return cleanupFunc, nil 298 } 299 } 300 301 func isDBContainerAvailable(hostname, port string) (isAvailable bool, dbCfg Config, err error) { 302 dbCfg = makeConnectionString(hostname, port) 303 304 connection, err := dbr.Open("postgres", dbCfg.ConnectionURL(), nil) 305 if err != nil { 306 return false, Config{}, fmt.Errorf("invalid connection string: %w", err) 307 } 308 309 defer func(c *dbr.Connection) { 310 err = c.Close() 311 if err != nil { 312 err = fmt.Errorf("failed to close database connection: %w", err) 313 } 314 }(connection) 315 316 err = connection.Ping() 317 if err == nil { 318 return true, dbCfg, nil 319 } 320 321 return false, Config{}, fmt.Errorf("while checking container availbility: %w", err) 322 } 323 324 func clearDBQuery() string { 325 return fmt.Sprintf("TRUNCATE TABLE %s, %s, %s, %s RESTART IDENTITY CASCADE", 326 postsql.InstancesTableName, 327 postsql.OperationTableName, 328 postsql.OrchestrationTableName, 329 postsql.RuntimeStateTableName, 330 ) 331 } 332 333 func createDbContainer(log func(format string, args ...interface{}), hostname string) (func(), Config, error) { 334 335 cli, err := dockerClient() 336 if err != nil { 337 return nil, Config{}, fmt.Errorf("while creating docker client: %w", err) 338 } 339 340 dbImage := "postgres:11" 341 342 filterBy := filters.NewArgs() 343 filterBy.Add("name", dbImage) 344 image, err := cli.ImageList(context.Background(), types.ImageListOptions{Filters: filterBy}) 345 346 if image == nil || err != nil { 347 log("Image not found... pulling...") 348 reader, err := cli.ImagePull(context.Background(), dbImage, types.ImagePullOptions{}) 349 io.Copy(os.Stdout, reader) 350 defer reader.Close() 351 352 if err != nil { 353 return nil, Config{}, fmt.Errorf("while pulling dbImage: %w", err) 354 } 355 } 356 357 _, parsedPortSpecs, err := nat.ParsePortSpecs([]string{DbPort}) 358 if err != nil { 359 return nil, Config{}, fmt.Errorf("while parsing ports specs: %w", err) 360 } 361 362 body, err := cli.ContainerCreate(context.Background(), 363 &container.Config{ 364 Image: dbImage, 365 Env: []string{ 366 fmt.Sprintf("POSTGRES_USER=%s", DbUser), 367 fmt.Sprintf("POSTGRES_PASSWORD=%s", DbPass), 368 fmt.Sprintf("POSTGRES_DB=%s", DbName), 369 }, 370 }, 371 &container.HostConfig{ 372 NetworkMode: "default", 373 PublishAllPorts: false, 374 PortBindings: parsedPortSpecs, 375 }, 376 &network.NetworkingConfig{ 377 EndpointsConfig: map[string]*network.EndpointSettings{ 378 DockerUserNetwork: { 379 Aliases: []string{ 380 hostname, 381 }, 382 }, 383 }, 384 }, 385 &v1.Platform{}, 386 "") 387 388 if err != nil { 389 return nil, Config{}, fmt.Errorf("during container creation: %w", err) 390 } 391 392 cleanupFunc := func() { 393 err := cli.ContainerRemove(context.Background(), body.ID, types.ContainerRemoveOptions{RemoveVolumes: true, RemoveLinks: false, Force: true}) 394 if err != nil { 395 panic(fmt.Errorf("during container removal: %w", err)) 396 } 397 } 398 399 if err := cli.ContainerStart(context.Background(), body.ID, types.ContainerStartOptions{}); err != nil { 400 return cleanupFunc, Config{}, fmt.Errorf("during container startup: %w", err) 401 } 402 403 err = waitFor(cli, body.ID, "database system is ready to accept connections") 404 if err != nil { 405 log("Failed to query container's logs: %s", err) 406 return cleanupFunc, Config{}, fmt.Errorf("while waiting for DB readiness: %w", err) 407 } 408 409 filterBy = filters.NewArgs() 410 filterBy.Add("id", body.ID) 411 containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{Filters: filterBy}) 412 413 if err != nil || len(containers) == 0 { 414 log("no containers found: %s", err) 415 return cleanupFunc, Config{}, fmt.Errorf("while loading containers: %w", err) 416 } 417 418 var container = &containers[0] 419 420 if container == nil { 421 log("no container found: %s", err) 422 return cleanupFunc, Config{}, fmt.Errorf("while searching for a container: %w", err) 423 } 424 425 ports := container.Ports 426 427 dbCfg := makeConnectionString(hostname, fmt.Sprint(ports[0].PublicPort)) 428 429 return cleanupFunc, dbCfg, nil 430 } 431 432 func waitFor(cli *client.Client, containerId string, text string) error { 433 return wait.PollImmediate(3*time.Second, 10*time.Second, func() (done bool, err error) { 434 out, err := cli.ContainerLogs(context.Background(), containerId, types.ContainerLogsOptions{ShowStdout: true}) 435 if err != nil { 436 return true, fmt.Errorf("while loading logs: %w", err) 437 } 438 439 bufReader := bufio.NewReader(out) 440 defer out.Close() 441 442 for line, isPrefix, err := bufReader.ReadLine(); err == nil; line, isPrefix, err = bufReader.ReadLine() { 443 if !isPrefix && strings.Contains(string(line), text) { 444 return true, nil 445 } 446 } 447 448 if err != nil { 449 return false, fmt.Errorf("while waiting for a container: %w", err) 450 } 451 452 return true, nil 453 }) 454 }