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  }