dev-tools/mage/integtest_docker.go (216 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package mage
import (
"fmt"
"go/build"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
var (
// StackEnvironment specifies what testing environment
// to use (like snapshot (default), latest, 5x). Formerly known as
// TESTING_ENVIRONMENT.
StackEnvironment = EnvOr("STACK_ENVIRONMENT", "snapshot")
)
func init() {
RegisterIntegrationTester(&DockerIntegrationTester{})
}
// DockerIntegrationTester returns build image
type DockerIntegrationTester struct {
buildImagesOnce sync.Once
}
// Name returns docker name.
func (d *DockerIntegrationTester) Name() string {
return "docker"
}
// Use determines if this tester should be used.
func (d *DockerIntegrationTester) Use(dir string) (bool, error) {
dockerFile := filepath.Join(dir, "docker-compose.yml")
if _, err := os.Stat(dockerFile); !os.IsNotExist(err) {
return true, nil
}
return false, nil
}
// HasRequirements ensures that the required docker and docker-compose are installed.
func (d *DockerIntegrationTester) HasRequirements() error {
if err := HaveDocker(); err != nil {
return err
}
if err := HaveDockerCompose(); err != nil {
return err
}
return nil
}
// StepRequirements returns the steps required for this tester.
func (d *DockerIntegrationTester) StepRequirements() IntegrationTestSteps {
return IntegrationTestSteps{&IntegrationTestStep{}}
}
// Test performs the tests with docker-compose.
func (d *DockerIntegrationTester) Test(dir string, mageTarget string, env map[string]string) error {
var err error
d.buildImagesOnce.Do(func() { err = dockerComposeBuildImages() })
if err != nil {
return err
}
// Determine the path to use inside the container.
repo, err := GetProjectRepoInfo()
if err != nil {
return err
}
dockerRepoRoot := filepath.Join("/go/src", repo.CanonicalRootImportPath)
dockerGoCache := filepath.Join(dockerRepoRoot, "build/docker-gocache")
magePath := filepath.Join("/go/src", repo.CanonicalRootImportPath, repo.SubDir, "build/mage-linux-"+GOARCH)
goPkgCache := filepath.Join(filepath.SplitList(build.Default.GOPATH)[0], "pkg/mod/cache/download")
dockerGoPkgCache := "/gocache"
// Execute the inside of docker-compose.
args := []string{"-p", dockerComposeProjectName(), "run",
"-e", "DOCKER_COMPOSE_PROJECT_NAME=" + dockerComposeProjectName(),
// Disable strict.perms because we mount host dirs inside containers
// and the UID/GID won't meet the strict requirements.
"-e", "BEAT_STRICT_PERMS=false",
// compose.EnsureUp needs to know the environment type.
"-e", "STACK_ENVIRONMENT=" + StackEnvironment,
"-e", "TESTING_ENVIRONMENT=" + StackEnvironment,
"-e", "GOCACHE=" + dockerGoCache,
// Use the host machine's pkg cache to minimize external downloads.
"-v", goPkgCache + ":" + dockerGoPkgCache + ":ro",
"-e", "GOPROXY=file://" + dockerGoPkgCache + ",direct",
// Do not set ES_USER or ES_PATH in this file unless you intend to override
// values set in all individual docker-compose files
// "-e", "ES_USER=admin",
// "-e", "ES_PASS=testing",
}
args, err = addUIDGidEnvArgs(args)
if err != nil {
return err
}
for envVame, envVal := range env {
args = append(args, "-e", fmt.Sprintf("%s=%s", envVame, envVal))
}
args = append(args,
"beat", // Docker compose container name.
magePath,
mageTarget,
)
composeEnv, err := integTestDockerComposeEnvVars()
if err != nil {
return err
}
_, testErr := sh.Exec(
composeEnv,
os.Stdout,
os.Stderr,
"docker-compose",
args...,
)
err = saveDockerComposeLogs(dir, mageTarget, composeEnv)
if err != nil && testErr == nil {
// saving docker-compose logs failed but the test didn't.
return err
}
// Docker-compose rm is noisy. So only pass through stderr when in verbose.
out := io.Discard
if mg.Verbose() {
out = os.Stderr
}
_, err = sh.Exec(
composeEnv,
io.Discard,
out,
"docker-compose",
"-p", dockerComposeProjectName(),
"rm", "--stop", "--force",
)
if err != nil && testErr == nil {
// docker-compose rm failed but the test didn't
return err
}
return testErr
}
func saveDockerComposeLogs(rootDir string, mageTarget string, composeEnv map[string]string) error {
var (
composeLogDir = filepath.Join(rootDir, "build", "system-tests", "docker-logs")
composeLogFileName = filepath.Join(composeLogDir, "TEST-docker-compose-"+mageTarget+".log")
)
if err := os.MkdirAll(composeLogDir, os.ModeDir|os.ModePerm); err != nil {
return fmt.Errorf("creating docker log dir: %w", err)
}
composeLogFile, err := os.Create(composeLogFileName)
if err != nil {
return fmt.Errorf("creating docker log file: %w", err)
}
defer composeLogFile.Close()
_, err = sh.Exec(
composeEnv,
composeLogFile, // stdout
composeLogFile, // stderr
"docker-compose",
"-p", dockerComposeProjectName(),
"logs",
"--no-color",
)
if err != nil {
return fmt.Errorf("executing docker-compose logs: %w", err)
}
return nil
}
// InsideTest performs the tests inside of environment.
func (d *DockerIntegrationTester) InsideTest(test func() error) error {
// Fix file permissions after test is done writing files as root.
if runtime.GOOS != "windows" {
repo, err := GetProjectRepoInfo()
if err != nil {
return err
}
// Handle virtualenv and the current project dir.
defer DockerChown(path.Join(repo.RootDir, "build"))
defer DockerChown(".")
}
return test()
}
// integTestDockerComposeEnvVars returns the environment variables used for
// executing docker-compose (not the variables passed into the containers).
// docker-compose uses these when evaluating docker-compose.yml files.
func integTestDockerComposeEnvVars() (map[string]string, error) {
esBeatsDir, err := ElasticBeatsDir()
if err != nil {
return nil, err
}
return map[string]string{
"ES_BEATS": esBeatsDir,
"STACK_ENVIRONMENT": StackEnvironment,
// Deprecated use STACK_ENVIRONMENT instead (it's more descriptive).
"TESTING_ENVIRONMENT": StackEnvironment,
}, nil
}
// dockerComposeProjectName returns the project name to use with docker-compose.
// It is passed to docker-compose using the `-p` flag. And is passed to our
// Go and Python testing libraries through the DOCKER_COMPOSE_PROJECT_NAME
// environment variable.
func dockerComposeProjectName() string {
commit, err := CommitHash()
if err != nil {
panic(fmt.Errorf("failed to construct docker compose project name: %w", err))
}
version, err := BeatQualifiedVersion()
if err != nil {
panic(fmt.Errorf("failed to construct docker compose project name: %w", err))
}
version = strings.NewReplacer(".", "_").Replace(version)
projectName := "{{.BeatName}}_{{.Version}}_{{.ShortCommit}}-{{.StackEnvironment}}"
projectName = MustExpand(projectName, map[string]interface{}{
"StackEnvironment": StackEnvironment,
"ShortCommit": commit[:10],
"Version": version,
})
return projectName
}
// dockerComposeBuildImages builds all images in the docker-compose.yml file.
func dockerComposeBuildImages() error {
fmt.Println(">> Building docker images")
composeEnv, err := integTestDockerComposeEnvVars()
if err != nil {
return err
}
args := []string{"-p", dockerComposeProjectName(), "build", "--force-rm"}
if _, noCache := os.LookupEnv("DOCKER_NOCACHE"); noCache {
args = append(args, "--no-cache")
}
if _, forcePull := os.LookupEnv("DOCKER_PULL"); forcePull {
args = append(args, "--pull")
}
out := io.Discard
if mg.Verbose() {
out = os.Stderr
}
_, err = sh.Exec(
composeEnv,
out,
os.Stderr,
"docker-compose", args...,
)
if err != nil {
fmt.Println(">> Building docker images again")
//nolint:staticcheck // This sleep is to avoid hitting the docker build issues when resources are not available.
time.Sleep(10)
_, err = sh.Exec(
composeEnv,
out,
os.Stderr,
"docker-compose", args...,
)
}
return err
}