2021-12-07 15:06:35 +02:00
//go:build integration
2020-07-20 18:07:08 +02:00
// +build integration
package main
import (
2022-02-25 17:38:02 +02:00
"archive/tar"
2022-09-19 12:47:13 +02:00
"bufio"
2020-07-20 18:07:08 +02:00
"bytes"
2020-07-21 11:15:46 +02:00
"fmt"
2022-02-25 17:38:02 +02:00
"io"
2020-07-20 18:07:08 +02:00
"io/ioutil"
"math/rand"
"os"
"path"
"strconv"
"strings"
"testing"
"time"
2021-12-07 15:06:35 +02:00
2022-04-14 14:10:32 +02:00
"github.com/magiconair/properties/assert"
"github.com/pkg/errors"
2021-12-07 15:06:35 +02:00
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
2020-07-20 18:07:08 +02:00
)
2020-07-22 15:48:46 +02:00
// The functions in this file provide a convenient way to integration test the piper binary in docker containers.
// It follows the "given, when, then" approach for structuring tests.
// The general concept is that per test one container is started, one piper command is run and outcomes are asserted.
// Please note that so far this was only tested with debian/ubuntu based containers.
//
// Non-exhaustive list of assumptions those functions make:
2022-02-25 17:38:02 +02:00
// - the following commands are available in the container: sh, chown, sleep
2020-07-22 15:48:46 +02:00
// - If the option TestDir is not provided, the test project must be in the container image in the directory /project
// IntegrationTestDockerExecRunnerBundle is used to construct an instance of IntegrationTestDockerExecRunner
// This is what a test uses to specify the container it requires
type IntegrationTestDockerExecRunnerBundle struct {
Image string
User string
TestDir [ ] string
Mounts map [ string ] string
Environment map [ string ] string
Setup [ ] string
2021-12-07 15:06:35 +02:00
Network string
2022-04-14 14:10:32 +02:00
ExecNoLogin bool
2020-07-22 15:48:46 +02:00
}
2020-07-20 18:07:08 +02:00
// IntegrationTestDockerExecRunner keeps the state of an instance of a docker runner
type IntegrationTestDockerExecRunner struct {
// Runner is the ExecRunner to which all executions are forwarded in the end.
Runner command . Command
Image string
User string
TestDir [ ] string
Mounts map [ string ] string
Environment map [ string ] string
Setup [ ] string
2021-12-07 15:06:35 +02:00
Network string
2020-07-20 18:07:08 +02:00
ContainerName string
2022-04-14 14:10:32 +02:00
ExecNoLogin bool
2020-07-20 18:07:08 +02:00
}
func givenThisContainer ( t * testing . T , bundle IntegrationTestDockerExecRunnerBundle ) IntegrationTestDockerExecRunner {
2022-09-19 12:47:13 +02:00
2020-07-20 18:07:08 +02:00
runner := command . Command { }
2020-07-22 15:48:46 +02:00
containerName := generateContainerName ( )
2020-07-20 18:07:08 +02:00
testRunner := IntegrationTestDockerExecRunner {
Runner : runner ,
Image : bundle . Image ,
User : bundle . User ,
Mounts : bundle . Mounts ,
2020-08-11 10:28:03 +02:00
Environment : bundle . Environment ,
2020-07-20 18:07:08 +02:00
Setup : bundle . Setup ,
2021-12-07 15:06:35 +02:00
Network : bundle . Network ,
2022-04-14 14:10:32 +02:00
ExecNoLogin : bundle . ExecNoLogin ,
2020-07-20 18:07:08 +02:00
ContainerName : containerName ,
}
wd , _ := os . Getwd ( )
localPiper := path . Join ( wd , ".." , "piper" )
if localPiper == "" {
t . Fatal ( "Could not locate piper binary to test" )
}
2020-07-21 11:15:46 +02:00
params := [ ] string { "run" , "--detach" , "-v" , localPiper + ":/piper" , "--name=" + testRunner . ContainerName }
if testRunner . User != "" {
params = append ( params , fmt . Sprintf ( "--user=%s" , testRunner . User ) )
2020-07-20 18:07:08 +02:00
}
2020-07-21 11:15:46 +02:00
if len ( bundle . TestDir ) > 0 {
projectDir := path . Join ( wd , path . Join ( bundle . TestDir ... ) )
// 1. Copy test files to a temp dir in order to avoid non-repeatable test executions because of changed state
// 2. Don't remove the temp dir to allow investigation of failed tests. Maybe add an option for cleaning it later?
tempDir , err := ioutil . TempDir ( "" , "piper-integration-test" )
if err != nil {
t . Fatal ( err )
}
2020-07-20 18:07:08 +02:00
2020-07-21 11:15:46 +02:00
err = copyDir ( projectDir , tempDir )
if err != nil {
t . Fatalf ( "Failed to copy files from %s into %s" , projectDir , tempDir )
}
params = append ( params , "-v" , fmt . Sprintf ( "%s:/project" , tempDir ) )
}
2021-12-07 15:06:35 +02:00
2020-07-21 11:15:46 +02:00
if len ( testRunner . Environment ) > 0 {
for envVarName , envVarValue := range testRunner . Environment {
2020-08-11 10:28:03 +02:00
params = append ( params , "--env" , fmt . Sprintf ( "%s=%s" , envVarName , envVarValue ) )
2020-07-21 11:15:46 +02:00
}
2020-07-20 18:07:08 +02:00
}
2021-12-07 15:06:35 +02:00
if testRunner . Mounts != nil {
2022-02-25 17:38:02 +02:00
wd , _ := os . Getwd ( )
2021-12-07 15:06:35 +02:00
for src , dst := range testRunner . Mounts {
2022-02-25 17:38:02 +02:00
localSrc := path . Join ( wd , src )
params = append ( params , "-v" , fmt . Sprintf ( "%s:%s" , localSrc , dst ) )
2021-12-07 15:06:35 +02:00
}
}
if testRunner . Network != "" {
params = append ( params , "--network" , testRunner . Network )
}
2020-07-21 11:15:46 +02:00
params = append ( params , testRunner . Image , "sleep" , "2000" )
2020-07-20 18:07:08 +02:00
2020-07-21 11:15:46 +02:00
err := testRunner . Runner . RunExecutable ( "docker" , params ... )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Starting test container has failed %s" , err )
}
2020-07-21 11:15:46 +02:00
2022-02-25 17:38:02 +02:00
if len ( bundle . TestDir ) > 0 && testRunner . User != "" {
2020-07-21 11:15:46 +02:00
err = testRunner . Runner . RunExecutable ( "docker" , "exec" , "-u=root" , testRunner . ContainerName , "chown" , "-R" , testRunner . User , "/project" )
if err != nil {
t . Fatalf ( "Chown /project has failed %s" , err )
}
}
2022-09-19 12:47:13 +02:00
if err = testRunner . Runner . RunExecutable (
"docker" , "exec" , testRunner . ContainerName , "sh" , "-c" ,
strings . Join ( testRunner . Setup , "\n" ) ,
) ; err != nil {
t . Fatalf ( "Running setup script in test container has failed %s" , err )
2020-07-20 18:07:08 +02:00
}
2020-07-22 15:48:46 +02:00
setupPiperBinary ( t , testRunner , localPiper )
return testRunner
}
2022-09-19 12:47:13 +02:00
// generateContainerName creates a name with a common prefix and a random number, so we can start a new container for each test method
2020-07-22 15:48:46 +02:00
// We don't rely on docker's random name generator for two reasons
// First, it is easier to save the name here compared to getting it from stdout
// Second, the common prefix allows batch stopping/deleting of containers if so desired
// The test code will not automatically delete containers as they might be useful for debugging
func generateContainerName ( ) string {
var seededRand = rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
return "piper-integration-test-" + strconv . Itoa ( seededRand . Int ( ) )
}
// setupPiperBinary copies a wrapper script for calling the piper binary into the container and verifies that the piper binary is executable inside the container
// The wrapper script (piper-command-wrapper.sh) only calls the piper binary and redirects its output into a file
// The purpose of this is to capture piper's stdout/stderr in order to assert on the output
// This is not possible via "docker logs", cf https://github.com/moby/moby/issues/8662
func setupPiperBinary ( t * testing . T , testRunner IntegrationTestDockerExecRunner , localPiper string ) {
err := testRunner . Runner . RunExecutable ( "docker" , "cp" , "piper-command-wrapper.sh" , testRunner . ContainerName + ":/piper-wrapper" )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Copying command wrapper to container has failed %s" , err )
}
2020-07-21 11:15:46 +02:00
err = testRunner . Runner . RunExecutable ( "docker" , "exec" , "-u=root" , testRunner . ContainerName , "chmod" , "+x" , "/piper-wrapper" )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Making command wrapper in container executable has failed %s" , err )
}
2022-02-25 17:38:02 +02:00
err = testRunner . Runner . RunExecutable ( "docker" , "exec" , testRunner . ContainerName , "/bin/sh" , "/piper-wrapper" , "/piper" , "version" )
2020-07-22 15:48:46 +02:00
if err != nil {
t . Fatalf ( "Running piper failed. " +
"Please check that '%s' is the correct binary, and is compiled for this configuration: 'GOOS=linux GOARCH=amd64'. Error text: %s" , localPiper , err )
}
2020-07-20 18:07:08 +02:00
}
func ( d * IntegrationTestDockerExecRunner ) whenRunningPiperCommand ( command string , parameters ... string ) error {
2022-04-14 14:10:32 +02:00
args := [ ] string { "exec" , "--workdir" , "/project" , d . ContainerName , "/bin/sh" }
if ! d . ExecNoLogin {
args = append ( args , "-l" )
}
args = append ( args , "/piper-wrapper" , "/piper" , command )
2022-09-19 12:47:13 +02:00
err := d . Runner . RunExecutable ( "docker" , append ( args , parameters ... ) ... )
2022-02-25 17:38:02 +02:00
if err != nil {
stdOut , err := d . getPiperOutput ( )
return errors . Wrapf ( err , "piper output: \n%s" , stdOut . String ( ) )
}
return err
2020-07-20 18:07:08 +02:00
}
2020-08-11 10:28:03 +02:00
func ( d * IntegrationTestDockerExecRunner ) runScriptInsideContainer ( script string ) error {
2022-04-14 14:10:32 +02:00
args := [ ] string { "exec" , "--workdir" , "/project" , d . ContainerName , "/bin/sh" }
if ! d . ExecNoLogin {
args = append ( args , "-l" )
}
args = append ( args , "-c" , script )
2020-08-11 10:28:03 +02:00
return d . Runner . RunExecutable ( "docker" , args ... )
}
2022-09-19 12:47:13 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertHasNoOutput ( t * testing . T , inconsistencies ... string ) {
count := len ( inconsistencies )
2022-04-14 14:10:32 +02:00
buffer , err := d . getPiperOutput ( )
if err != nil {
t . Fatalf ( "Failed to get log output of container %s" , d . ContainerName )
}
2022-09-19 12:47:13 +02:00
scanner := bufio . NewScanner ( buffer )
for scanner . Scan ( ) && ( len ( inconsistencies ) != 0 ) {
for i , str := range inconsistencies {
if strings . Contains ( scanner . Text ( ) , str ) {
inconsistencies = append ( inconsistencies [ : i ] , inconsistencies [ i + 1 : ] ... )
break
}
}
2022-04-14 14:10:32 +02:00
}
2022-09-19 12:47:13 +02:00
assert . Equal ( t , len ( inconsistencies ) , count , fmt . Sprintf (
"[assertHasNoOutput] Unexpected command output:\n%s\n%s\n" , buffer . String ( ) , strings . Join ( inconsistencies , "\n" ) ) ,
)
2022-04-14 14:10:32 +02:00
}
2022-09-19 12:47:13 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertHasOutput ( t * testing . T , consistencies ... string ) {
2022-02-25 17:38:02 +02:00
buffer , err := d . getPiperOutput ( )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Failed to get log output of container %s" , d . ContainerName )
}
2022-09-19 12:47:13 +02:00
scanner := bufio . NewScanner ( buffer )
for scanner . Scan ( ) && ( len ( consistencies ) != 0 ) {
for i , str := range consistencies {
if strings . Contains ( scanner . Text ( ) , str ) {
consistencies = append ( consistencies [ : i ] , consistencies [ i + 1 : ] ... )
break
}
}
2020-07-20 18:07:08 +02:00
}
2022-09-19 12:47:13 +02:00
assert . Equal ( t , len ( consistencies ) , 0 , fmt . Sprintf (
"[assertHasOutput] Unexpected command output:\n%s\n%s\n" , buffer . String ( ) , strings . Join ( consistencies , "\n" ) ) ,
)
2020-07-20 18:07:08 +02:00
}
2020-07-21 11:15:46 +02:00
2022-02-25 17:38:02 +02:00
func ( d * IntegrationTestDockerExecRunner ) getPiperOutput ( ) ( * bytes . Buffer , error ) {
buffer := new ( bytes . Buffer )
d . Runner . Stdout ( buffer )
err := d . Runner . RunExecutable ( "docker" , "exec" , d . ContainerName , "cat" , "/tmp/test-log.txt" )
d . Runner . Stdout ( log . Writer ( ) )
return buffer , err
}
2022-09-19 12:47:13 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertHasFiles ( t * testing . T , consistencies ... string ) {
buffer := new ( bytes . Buffer )
d . Runner . Stderr ( buffer )
if d . Runner . RunExecutable (
"docker" ,
append ( append ( make ( [ ] string , 0 ) , "exec" , d . ContainerName , "stat" ) , consistencies ... ) ... ,
) != nil {
t . Fatalf ( "[assertHasFiles] Assertion has failed: %v" , errors . New ( buffer . String ( ) ) )
2020-07-21 11:15:46 +02:00
}
}
2021-12-07 15:06:35 +02:00
2022-02-25 17:38:02 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertFileContentEquals ( t * testing . T , fileWant string , contentWant string ) {
2022-09-19 12:47:13 +02:00
d . assertHasFiles ( t , fileWant )
2022-02-25 17:38:02 +02:00
buffer := new ( bytes . Buffer )
d . Runner . Stdout ( buffer )
err := d . Runner . RunExecutable ( "docker" , "cp" , d . ContainerName + ":/" + fileWant , "-" )
if err != nil {
t . Fatalf ( "Copy file has failed. Expected file %s to exist in container. %s" , fileWant , err )
}
tarReader := tar . NewReader ( buffer )
header , err := tarReader . Next ( )
if err == io . EOF {
t . Fatal ( "Empty tar received" )
}
if err != nil {
t . Fatalf ( "Cant read tar: %s" , err )
}
if header . Typeflag != tar . TypeReg {
t . Fatalf ( "Expected a file, but received %c" , header . Typeflag )
}
str := new ( bytes . Buffer )
_ , err = io . Copy ( str , tarReader )
if err != nil {
t . Fatalf ( "unable to get tar file content: %s" , err )
}
if ! strings . Contains ( str . String ( ) , contentWant ) {
assert . Equal ( t , str . String ( ) , contentWant , fmt . Sprintf ( "Unexpected content of file '%s'" , fileWant ) )
}
}
2021-12-07 15:06:35 +02:00
func ( d * IntegrationTestDockerExecRunner ) terminate ( t * testing . T ) {
err := d . Runner . RunExecutable ( "docker" , "rm" , "-f" , d . ContainerName )
if err != nil {
t . Fatalf ( "Failed to terminate container '%s'" , d . ContainerName )
}
}