123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- /*
- Copyright 2020 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package e2e
- import (
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "runtime"
- "strings"
- "testing"
- "time"
- "gotest.tools/v3/assert"
- is "gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/icmd"
- "gotest.tools/v3/poll"
- )
- var (
- // DockerExecutableName is the OS dependent Docker CLI binary name
- DockerExecutableName = "docker"
- existingExectuableName = "com.docker.cli"
- )
- func init() {
- if runtime.GOOS == "windows" {
- DockerExecutableName = DockerExecutableName + ".exe"
- existingExectuableName = existingExectuableName + ".exe"
- }
- }
- // E2eCLI is used to wrap the CLI for end to end testing
- // nolint stutter
- type E2eCLI struct {
- BinDir string
- ConfigDir string
- test *testing.T
- }
- // NewParallelE2eCLI returns a configured TestE2eCLI with t.Parallel() set
- func NewParallelE2eCLI(t *testing.T, binDir string) *E2eCLI {
- t.Parallel()
- return newE2eCLI(t, binDir)
- }
- // NewE2eCLI returns a configured TestE2eCLI
- func NewE2eCLI(t *testing.T, binDir string) *E2eCLI {
- return newE2eCLI(t, binDir)
- }
- func newE2eCLI(t *testing.T, binDir string) *E2eCLI {
- d, err := ioutil.TempDir("", "")
- assert.Check(t, is.Nil(err))
- t.Cleanup(func() {
- if t.Failed() {
- conf, _ := ioutil.ReadFile(filepath.Join(d, "config.json"))
- t.Errorf("Config: %s\n", string(conf))
- t.Error("Contents of config dir:")
- for _, p := range dirContents(d) {
- t.Errorf(p)
- }
- }
- _ = os.RemoveAll(d)
- })
- _ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755)
- composePluginFile := "docker-compose"
- if runtime.GOOS == "windows" {
- composePluginFile += ".exe"
- }
- composePlugin, _ := findExecutable(composePluginFile, []string{"../../bin", "../../../bin"})
- err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", composePluginFile))
- if err != nil {
- panic(err)
- }
- return &E2eCLI{binDir, d, t}
- }
- func dirContents(dir string) []string {
- res := []string{}
- _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- res = append(res, filepath.Join(dir, path))
- return nil
- })
- return res
- }
- // SetupExistingCLI copies the existing CLI in a temporary directory so that the
- // new CLI can be configured to use it
- func SetupExistingCLI() (string, func(), error) {
- p, err := exec.LookPath(existingExectuableName)
- if err != nil {
- p, err = exec.LookPath(DockerExecutableName)
- if err != nil {
- return "", nil, errors.New("existing CLI not found in PATH")
- }
- }
- d, err := ioutil.TempDir("", "")
- if err != nil {
- return "", nil, err
- }
- if err := CopyFile(p, filepath.Join(d, existingExectuableName)); err != nil {
- return "", nil, err
- }
- bin, err := findExecutable(DockerExecutableName, []string{"../../bin", "../../../bin"})
- if err != nil {
- return "", nil, err
- }
- if err := CopyFile(bin, filepath.Join(d, DockerExecutableName)); err != nil {
- return "", nil, err
- }
- cleanup := func() {
- _ = os.RemoveAll(d)
- }
- return d, cleanup, nil
- }
- func findExecutable(executableName string, paths []string) (string, error) {
- for _, p := range paths {
- bin, err := filepath.Abs(path.Join(p, executableName))
- if err != nil {
- return "", err
- }
- if _, err := os.Stat(bin); os.IsNotExist(err) {
- continue
- }
- return bin, nil
- }
- return "", errors.New("executable not found")
- }
- // CopyFile copies a file from a sourceFile to a destinationFile setting permissions to 0755
- func CopyFile(sourceFile string, destinationFile string) error {
- src, err := os.Open(sourceFile)
- if err != nil {
- return err
- }
- // nolint: errcheck
- defer src.Close()
- dst, err := os.OpenFile(destinationFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
- if err != nil {
- return err
- }
- // nolint: errcheck
- defer dst.Close()
- if _, err = io.Copy(dst, src); err != nil {
- return err
- }
- return err
- }
- // NewCmd creates a cmd object configured with the test environment set
- func (c *E2eCLI) NewCmd(command string, args ...string) icmd.Cmd {
- env := append(os.Environ(),
- "DOCKER_CONFIG="+c.ConfigDir,
- "KUBECONFIG=invalid",
- "TEST_METRICS_SOCKET="+c.MetricsSocket(),
- "PATH="+c.PathEnvVar(),
- )
- return icmd.Cmd{
- Command: append([]string{command}, args...),
- Env: env,
- }
- }
- // MetricsSocket get the path where test metrics will be sent
- func (c *E2eCLI) MetricsSocket() string {
- return filepath.Join(c.ConfigDir, "./docker-cli.sock")
- }
- // NewDockerCmd creates a docker cmd without running it
- func (c *E2eCLI) NewDockerCmd(args ...string) icmd.Cmd {
- return c.NewCmd(filepath.Join(c.BinDir, DockerExecutableName), args...)
- }
- // RunDockerOrExitError runs a docker command and returns a result
- func (c *E2eCLI) RunDockerOrExitError(args ...string) *icmd.Result {
- fmt.Printf(" [%s] docker %s\n", c.test.Name(), strings.Join(args, " "))
- return icmd.RunCmd(c.NewDockerCmd(args...))
- }
- // RunCmd runs a command, expects no error and returns a result
- func (c *E2eCLI) RunCmd(args ...string) *icmd.Result {
- fmt.Printf(" [%s] %s\n", c.test.Name(), strings.Join(args, " "))
- assert.Assert(c.test, len(args) >= 1, "require at least one command in parameters")
- res := icmd.RunCmd(c.NewCmd(args[0], args[1:]...))
- res.Assert(c.test, icmd.Success)
- return res
- }
- // RunDockerCmd runs a docker command, expects no error and returns a result
- func (c *E2eCLI) RunDockerCmd(args ...string) *icmd.Result {
- res := c.RunDockerOrExitError(args...)
- res.Assert(c.test, icmd.Success)
- return res
- }
- // StdoutContains returns a predicate on command result expecting a string in stdout
- func StdoutContains(expected string) func(*icmd.Result) bool {
- return func(res *icmd.Result) bool {
- return strings.Contains(res.Stdout(), expected)
- }
- }
- // WaitForCmdResult try to execute a cmd until resulting output matches given predicate
- func (c *E2eCLI) WaitForCmdResult(command icmd.Cmd, predicate func(*icmd.Result) bool, timeout time.Duration, delay time.Duration) {
- assert.Assert(c.test, timeout.Nanoseconds() > delay.Nanoseconds(), "timeout must be greater than delay")
- var res *icmd.Result
- checkStopped := func(logt poll.LogT) poll.Result {
- fmt.Printf(" [%s] %s\n", c.test.Name(), strings.Join(command.Command, " "))
- res = icmd.RunCmd(command)
- if !predicate(res) {
- return poll.Continue("Cmd output did not match requirement: %q", res.Combined())
- }
- return poll.Success()
- }
- poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
- }
- // PathEnvVar returns path (os sensitive) for running test
- func (c *E2eCLI) PathEnvVar() string {
- path := c.BinDir + ":" + os.Getenv("PATH")
- if runtime.GOOS == "windows" {
- path = c.BinDir + ";" + os.Getenv("PATH")
- }
- return path
- }
- // GoldenFile golden file specific to platform
- func GoldenFile(name string) string {
- if runtime.GOOS == "windows" {
- return name + "-windows.golden"
- }
- return name + ".golden"
- }
- //Lines split output into lines
- func Lines(output string) []string {
- return strings.Split(strings.TrimSpace(output), "\n")
- }
- // HTTPGetWithRetry performs an HTTP GET on an `endpoint`, using retryDelay also as a request timeout.
- // In the case of an error or the response status is not the expeted one, it retries the same request,
- // returning the response body as a string (empty if we could not reach it)
- func HTTPGetWithRetry(t *testing.T, endpoint string, expectedStatus int, retryDelay time.Duration, timeout time.Duration) string {
- var (
- r *http.Response
- err error
- )
- client := &http.Client{
- Timeout: retryDelay,
- }
- fmt.Printf(" [%s] GET %s\n", t.Name(), endpoint)
- checkUp := func(t poll.LogT) poll.Result {
- r, err = client.Get(endpoint)
- if err != nil {
- return poll.Continue("reaching %q: Error %s", endpoint, err.Error())
- }
- if r.StatusCode == expectedStatus {
- return poll.Success()
- }
- return poll.Continue("reaching %q: %d != %d", endpoint, r.StatusCode, expectedStatus)
- }
- poll.WaitOn(t, checkUp, poll.WithDelay(retryDelay), poll.WithTimeout(timeout))
- if r != nil {
- b, err := ioutil.ReadAll(r.Body)
- assert.NilError(t, err)
- return string(b)
- }
- return ""
- }
|