github.com/openshift-online/ocm-sdk-go@v0.1.473/testing/database.go (about)

     1  /*
     2  Copyright (c) 2021 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package testing
    18  
    19  import (
    20  	"bytes"
    21  	"database/sql"
    22  	"fmt"
    23  	"net"
    24  	"os/exec"
    25  	"strings"
    26  
    27  	"github.com/google/uuid"
    28  
    29  	. "github.com/onsi/ginkgo/v2/dsl/core" // nolint
    30  	. "github.com/onsi/gomega"             // nolint
    31  
    32  	_ "github.com/jackc/pgx/v4/stdlib" // nolint
    33  )
    34  
    35  // DatabaseServer knows how to start a PostgreSQL database server inside a container, and how to
    36  // create databases to be used for tests.
    37  type DatabaseServer struct {
    38  	// Name of the tool used to create containers (podman or docker):
    39  	tool string
    40  
    41  	// Identifier of the container where the database server is running:
    42  	container string
    43  
    44  	// Host and port of the database server:
    45  	host string
    46  	port string
    47  
    48  	// Database handle:
    49  	handle *sql.DB
    50  
    51  	// Number of databases created:
    52  	count int
    53  
    54  	// List of databases created, so we don't forget to remove them:
    55  	dbs []*Database
    56  }
    57  
    58  // Database is a PostgreSQL database.
    59  type Database struct {
    60  	// Reference to the database server that owns this database:
    61  	server *DatabaseServer
    62  
    63  	// Database name, user and password:
    64  	name     string
    65  	user     string
    66  	password string
    67  }
    68  
    69  // MakeDatabaseServer creates a new database server.
    70  func MakeDatabaseServer() *DatabaseServer {
    71  	var err error
    72  
    73  	// Check if podman or docker are available:
    74  	tool, err := exec.LookPath("podman")
    75  	if err != nil {
    76  		tool, err = exec.LookPath("docker")
    77  		Expect(err).ToNot(HaveOccurred(), "Can't find 'podman' or 'docker'")
    78  	}
    79  
    80  	// Generate a random password for the database admnistrator:
    81  	password := uuid.NewString()
    82  
    83  	// Start the database server:
    84  	runOut := &bytes.Buffer{}
    85  	runCmd := exec.Command(
    86  		tool, "run",
    87  		"--env", "POSTGRES_PASSWORD="+password,
    88  		"--publish", "5432",
    89  		"--detach",
    90  		"docker.io/postgres:14",
    91  		"-c", "log_destination=stderr",
    92  		"-c", "log_statement=all",
    93  		"-c", "logging_collector=off",
    94  	) // #nosec G204
    95  	runCmd.Stdout = runOut
    96  	runCmd.Stderr = GinkgoWriter
    97  	err = runCmd.Run()
    98  	Expect(err).ToNot(HaveOccurred())
    99  	container := strings.TrimSpace(runOut.String())
   100  
   101  	// Find out the port number assigned to the database server:
   102  	portOut := &bytes.Buffer{}
   103  	portCmd := exec.Command(tool, "port", container, "5432/tcp") // #nosec G204
   104  	portCmd.Stdout = portOut
   105  	portCmd.Stderr = GinkgoWriter
   106  	err = portCmd.Run()
   107  	Expect(err).ToNot(HaveOccurred())
   108  	portLines := strings.Split(portOut.String(), "\n")
   109  	Expect(len(portLines)).To(BeNumerically(">=", 1))
   110  	portLine := portLines[0]
   111  	hostPort := strings.TrimSpace(portLine)
   112  	host, port, err := net.SplitHostPort(hostPort)
   113  	Expect(err).ToNot(HaveOccurred())
   114  	if host == "0.0.0.0" {
   115  		host = "127.0.0.1"
   116  	}
   117  
   118  	// Wait till the database server is responding:
   119  	url := fmt.Sprintf(
   120  		"postgres://postgres:%s@%s:%s/postgres?sslmode=disable",
   121  		password, host, port,
   122  	)
   123  	handle, err := sql.Open("pgx", url)
   124  	Expect(err).ToNot(HaveOccurred())
   125  	Eventually(handle.Ping, 10, 1).ShouldNot(HaveOccurred())
   126  
   127  	// Create and populate the object:
   128  	return &DatabaseServer{
   129  		tool:      tool,
   130  		container: container,
   131  		host:      host,
   132  		port:      port,
   133  		handle:    handle,
   134  	}
   135  }
   136  
   137  // Close stops the database server.
   138  func (s *DatabaseServer) Close() {
   139  	var err error
   140  
   141  	// Delete all databases:
   142  	for _, db := range s.dbs {
   143  		db.Close()
   144  	}
   145  
   146  	// Get the logs of the database server:
   147  	logsCmd := exec.Command(s.tool, "logs", s.container) // #nosec G204
   148  	logsCmd.Stdout = GinkgoWriter
   149  	logsCmd.Stderr = GinkgoWriter
   150  	err = logsCmd.Run()
   151  	Expect(err).ToNot(HaveOccurred())
   152  
   153  	// Stop the database server:
   154  	killCmd := exec.Command(s.tool, "kill", s.container) // #nosec G204
   155  	killCmd.Stdout = GinkgoWriter
   156  	killCmd.Stderr = GinkgoWriter
   157  	err = killCmd.Run()
   158  	Expect(err).ToNot(HaveOccurred())
   159  }
   160  
   161  // MakeDatabase creates a new database.
   162  func (s *DatabaseServer) MakeDatabase() *Database {
   163  	var err error
   164  
   165  	// Generate the database name and password:
   166  	name := fmt.Sprintf("test_%d", s.count)
   167  	user := fmt.Sprintf("test_%d", s.count)
   168  	password := uuid.NewString()
   169  	s.count++
   170  
   171  	// Create the user:
   172  	_, err = s.handle.Exec(fmt.Sprintf(
   173  		"create user %s with password '%s'",
   174  		user, password,
   175  	))
   176  	Expect(err).ToNot(HaveOccurred())
   177  
   178  	// Create the database:
   179  	_, err = s.handle.Exec(fmt.Sprintf(
   180  		"create database %s owner %s;",
   181  		name, user,
   182  	))
   183  	Expect(err).ToNot(HaveOccurred())
   184  
   185  	// Create and populate the object:
   186  	db := &Database{
   187  		server:   s,
   188  		name:     name,
   189  		user:     user,
   190  		password: password,
   191  	}
   192  
   193  	// Remember to remove it:
   194  	s.dbs = append(s.dbs, db)
   195  
   196  	return db
   197  }
   198  
   199  // MakeHandle creates a new database handle for this database.
   200  func (d *Database) MakeHandle() *sql.DB {
   201  	url := fmt.Sprintf(
   202  		"postgres://%s:%s@%s:%s/%s?sslmode=disable",
   203  		d.user, d.password, d.server.host, d.server.port, d.name,
   204  	)
   205  	handle, err := sql.Open("pgx", url)
   206  	Expect(err).ToNot(HaveOccurred())
   207  	return handle
   208  }
   209  
   210  // Close deletes the database.
   211  func (d *Database) Close() {
   212  	var err error
   213  
   214  	// Drop the database:
   215  	_, err = d.server.handle.Exec(fmt.Sprintf(
   216  		`drop database if exists %s with (force)`,
   217  		d.name,
   218  	))
   219  	Expect(err).ToNot(HaveOccurred())
   220  
   221  	// Drop the user:
   222  	_, err = d.server.handle.Exec(fmt.Sprintf(
   223  		`drop user if exists %s`,
   224  		d.user,
   225  	))
   226  	Expect(err).ToNot(HaveOccurred())
   227  }