github.com/hernad/nomad@v1.6.112/drivers/docker/docklog/docker_logger_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package docklog
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	docker "github.com/fsouza/go-dockerclient"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/hernad/nomad/ci"
    19  	ctu "github.com/hernad/nomad/client/testutil"
    20  	"github.com/hernad/nomad/helper/testlog"
    21  	"github.com/hernad/nomad/testutil"
    22  )
    23  
    24  func testContainerDetails() (image string, imageName string, imageTag string) {
    25  	image = testutil.TestBusyboxImage()
    26  	parts := strings.Split(image, ":")
    27  	imageName = parts[0]
    28  	imageTag = parts[1]
    29  
    30  	return image, imageName, imageTag
    31  }
    32  
    33  func TestDockerLogger_Success(t *testing.T) {
    34  	ci.Parallel(t)
    35  	ctu.DockerCompatible(t)
    36  
    37  	require := require.New(t)
    38  
    39  	containerImage, containerImageName, containerImageTag := testContainerDetails()
    40  
    41  	client, err := docker.NewClientFromEnv()
    42  	if err != nil {
    43  		t.Skip("docker unavailable:", err)
    44  	}
    45  
    46  	if img, err := client.InspectImage(containerImage); err != nil || img == nil {
    47  		t.Log("image not found locally, downloading...")
    48  		err = client.PullImage(docker.PullImageOptions{
    49  			Repository: containerImageName,
    50  			Tag:        containerImageTag,
    51  		}, docker.AuthConfiguration{})
    52  		require.NoError(err, "failed to pull image")
    53  	}
    54  
    55  	containerConf := docker.CreateContainerOptions{
    56  		Config: &docker.Config{
    57  			Cmd: []string{
    58  				"sh", "-c", "touch ~/docklog; tail -f ~/docklog",
    59  			},
    60  			Image: containerImage,
    61  		},
    62  		Context: context.Background(),
    63  	}
    64  
    65  	container, err := client.CreateContainer(containerConf)
    66  	require.NoError(err)
    67  
    68  	defer client.RemoveContainer(docker.RemoveContainerOptions{
    69  		ID:    container.ID,
    70  		Force: true,
    71  	})
    72  
    73  	err = client.StartContainer(container.ID, nil)
    74  	require.NoError(err)
    75  
    76  	testutil.WaitForResult(func() (bool, error) {
    77  		container, err = client.InspectContainer(container.ID)
    78  		if err != nil {
    79  			return false, err
    80  		}
    81  		if !container.State.Running {
    82  			return false, fmt.Errorf("container not running")
    83  		}
    84  		return true, nil
    85  	}, func(err error) {
    86  		require.NoError(err)
    87  	})
    88  
    89  	stdout := &noopCloser{bytes.NewBuffer(nil)}
    90  	stderr := &noopCloser{bytes.NewBuffer(nil)}
    91  
    92  	dl := NewDockerLogger(testlog.HCLogger(t)).(*dockerLogger)
    93  	dl.stdout = stdout
    94  	dl.stderr = stderr
    95  	require.NoError(dl.Start(&StartOpts{
    96  		ContainerID: container.ID,
    97  	}))
    98  
    99  	echoToContainer(t, client, container.ID, "abc")
   100  	echoToContainer(t, client, container.ID, "123")
   101  
   102  	testutil.WaitForResult(func() (bool, error) {
   103  		act := stdout.String()
   104  		if "abc\n123\n" != act {
   105  			return false, fmt.Errorf("expected abc\\n123\\n for stdout but got %s", act)
   106  		}
   107  
   108  		return true, nil
   109  	}, func(err error) {
   110  		require.NoError(err)
   111  	})
   112  }
   113  
   114  func TestDockerLogger_Success_TTY(t *testing.T) {
   115  	ci.Parallel(t)
   116  	ctu.DockerCompatible(t)
   117  
   118  	require := require.New(t)
   119  
   120  	containerImage, containerImageName, containerImageTag := testContainerDetails()
   121  
   122  	client, err := docker.NewClientFromEnv()
   123  	if err != nil {
   124  		t.Skip("docker unavailable:", err)
   125  	}
   126  
   127  	if img, err := client.InspectImage(containerImage); err != nil || img == nil {
   128  		t.Log("image not found locally, downloading...")
   129  		err = client.PullImage(docker.PullImageOptions{
   130  			Repository: containerImageName,
   131  			Tag:        containerImageTag,
   132  		}, docker.AuthConfiguration{})
   133  		require.NoError(err, "failed to pull image")
   134  	}
   135  
   136  	containerConf := docker.CreateContainerOptions{
   137  		Config: &docker.Config{
   138  			Cmd: []string{
   139  				"sh", "-c", "touch ~/docklog; tail -f ~/docklog",
   140  			},
   141  			Image: containerImage,
   142  			Tty:   true,
   143  		},
   144  		Context: context.Background(),
   145  	}
   146  
   147  	container, err := client.CreateContainer(containerConf)
   148  	require.NoError(err)
   149  
   150  	defer client.RemoveContainer(docker.RemoveContainerOptions{
   151  		ID:    container.ID,
   152  		Force: true,
   153  	})
   154  
   155  	err = client.StartContainer(container.ID, nil)
   156  	require.NoError(err)
   157  
   158  	testutil.WaitForResult(func() (bool, error) {
   159  		container, err = client.InspectContainer(container.ID)
   160  		if err != nil {
   161  			return false, err
   162  		}
   163  		if !container.State.Running {
   164  			return false, fmt.Errorf("container not running")
   165  		}
   166  		return true, nil
   167  	}, func(err error) {
   168  		require.NoError(err)
   169  	})
   170  
   171  	stdout := &noopCloser{bytes.NewBuffer(nil)}
   172  	stderr := &noopCloser{bytes.NewBuffer(nil)}
   173  
   174  	dl := NewDockerLogger(testlog.HCLogger(t)).(*dockerLogger)
   175  	dl.stdout = stdout
   176  	dl.stderr = stderr
   177  	require.NoError(dl.Start(&StartOpts{
   178  		ContainerID: container.ID,
   179  		TTY:         true,
   180  	}))
   181  
   182  	echoToContainer(t, client, container.ID, "abc")
   183  	echoToContainer(t, client, container.ID, "123")
   184  
   185  	testutil.WaitForResult(func() (bool, error) {
   186  		act := stdout.String()
   187  		if "abc\r\n123\r\n" != act {
   188  			return false, fmt.Errorf("expected abc\\n123\\n for stdout but got %s", act)
   189  		}
   190  
   191  		return true, nil
   192  	}, func(err error) {
   193  		require.NoError(err)
   194  	})
   195  }
   196  
   197  func echoToContainer(t *testing.T, client *docker.Client, id string, line string) {
   198  	op := docker.CreateExecOptions{
   199  		Container: id,
   200  		Cmd: []string{
   201  			"/bin/sh", "-c",
   202  			fmt.Sprintf("echo %s >>~/docklog", line),
   203  		},
   204  	}
   205  
   206  	exec, err := client.CreateExec(op)
   207  	require.NoError(t, err)
   208  	require.NoError(t, client.StartExec(exec.ID, docker.StartExecOptions{Detach: true}))
   209  }
   210  
   211  func TestDockerLogger_LoggingNotSupported(t *testing.T) {
   212  	ci.Parallel(t)
   213  	ctu.DockerCompatible(t)
   214  
   215  	containerImage, containerImageName, containerImageTag := testContainerDetails()
   216  
   217  	client, err := docker.NewClientFromEnv()
   218  	if err != nil {
   219  		t.Skip("docker unavailable:", err)
   220  	}
   221  
   222  	if img, err := client.InspectImage(containerImage); err != nil || img == nil {
   223  		t.Log("image not found locally, downloading...")
   224  		err = client.PullImage(docker.PullImageOptions{
   225  			Repository: containerImageName,
   226  			Tag:        containerImageTag,
   227  		}, docker.AuthConfiguration{})
   228  		require.NoError(t, err, "failed to pull image")
   229  	}
   230  
   231  	containerConf := docker.CreateContainerOptions{
   232  		Config: &docker.Config{
   233  			Cmd: []string{
   234  				"sh", "-c", "touch ~/docklog; tail -f ~/docklog",
   235  			},
   236  			Image: containerImage,
   237  		},
   238  		HostConfig: &docker.HostConfig{
   239  			LogConfig: docker.LogConfig{
   240  				Type:   "none",
   241  				Config: map[string]string{},
   242  			},
   243  		},
   244  		Context: context.Background(),
   245  	}
   246  
   247  	container, err := client.CreateContainer(containerConf)
   248  	require.NoError(t, err)
   249  
   250  	defer client.RemoveContainer(docker.RemoveContainerOptions{
   251  		ID:    container.ID,
   252  		Force: true,
   253  	})
   254  
   255  	err = client.StartContainer(container.ID, nil)
   256  	require.NoError(t, err)
   257  
   258  	testutil.WaitForResult(func() (bool, error) {
   259  		container, err = client.InspectContainer(container.ID)
   260  		if err != nil {
   261  			return false, err
   262  		}
   263  		if !container.State.Running {
   264  			return false, fmt.Errorf("container not running")
   265  		}
   266  		return true, nil
   267  	}, func(err error) {
   268  		require.NoError(t, err)
   269  	})
   270  
   271  	stdout := &noopCloser{bytes.NewBuffer(nil)}
   272  	stderr := &noopCloser{bytes.NewBuffer(nil)}
   273  
   274  	dl := NewDockerLogger(testlog.HCLogger(t)).(*dockerLogger)
   275  	dl.stdout = stdout
   276  	dl.stderr = stderr
   277  	require.NoError(t, dl.Start(&StartOpts{
   278  		ContainerID: container.ID,
   279  	}))
   280  
   281  	select {
   282  	case <-dl.doneCh:
   283  	case <-time.After(10 * time.Second):
   284  		require.Fail(t, "timeout while waiting for docker_logging to terminate")
   285  	}
   286  }
   287  
   288  type noopCloser struct {
   289  	*bytes.Buffer
   290  }
   291  
   292  func (*noopCloser) Close() error {
   293  	return nil
   294  }
   295  
   296  func TestNextBackoff(t *testing.T) {
   297  	ci.Parallel(t)
   298  
   299  	cases := []struct {
   300  		currentBackoff float64
   301  		min            float64
   302  		max            float64
   303  	}{
   304  		{0.0, 0.5, 1.15},
   305  		{5.0, 5.0, 16},
   306  		{120, 120, 120},
   307  	}
   308  
   309  	for _, c := range cases {
   310  		t.Run(fmt.Sprintf("case %v", c.currentBackoff), func(t *testing.T) {
   311  			next := nextBackoff(c.currentBackoff)
   312  			t.Logf("computed backoff(%v) = %v", c.currentBackoff, next)
   313  
   314  			require.True(t, next >= c.min, "next backoff is smaller than expected")
   315  			require.True(t, next <= c.max, "next backoff is larger than expected")
   316  		})
   317  	}
   318  }
   319  
   320  func TestIsLoggingTerminalError(t *testing.T) {
   321  	ci.Parallel(t)
   322  
   323  	terminalErrs := []error{
   324  		errors.New("docker returned: configured logging driver does not support reading"),
   325  		&docker.Error{
   326  			Status:  501,
   327  			Message: "configured logging driver does not support reading",
   328  		},
   329  		&docker.Error{
   330  			Status:  501,
   331  			Message: "not implemented",
   332  		},
   333  	}
   334  
   335  	for _, err := range terminalErrs {
   336  		require.Truef(t, isLoggingTerminalError(err), "error should be terminal: %v", err)
   337  	}
   338  
   339  	nonTerminalErrs := []error{
   340  		errors.New("not expected"),
   341  		&docker.Error{
   342  			Status:  503,
   343  			Message: "Service Unavailable",
   344  		},
   345  	}
   346  
   347  	for _, err := range nonTerminalErrs {
   348  		require.Falsef(t, isLoggingTerminalError(err), "error should be terminal: %v", err)
   349  	}
   350  }