github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/utils/docker_test.go (about)

     1  package utils
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"net/http"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/docker/docker/api/types"
    11  	"github.com/docker/docker/api/types/container"
    12  	"github.com/docker/docker/pkg/jsonmessage"
    13  	"github.com/docker/docker/pkg/stdcopy"
    14  	"github.com/spf13/viper"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/Redstoneguy129/cli/internal/testing/apitest"
    18  	"gopkg.in/h2non/gock.v1"
    19  )
    20  
    21  const (
    22  	version     = "1.41"
    23  	containerId = "test-container"
    24  	imageId     = "test-image"
    25  )
    26  
    27  func TestPullImage(t *testing.T) {
    28  	viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io")
    29  
    30  	t.Run("pulls image if missing", func(t *testing.T) {
    31  		// Setup mock docker
    32  		require.NoError(t, apitest.MockDocker(Docker))
    33  		defer gock.OffAll()
    34  		gock.New(Docker.DaemonHost()).
    35  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
    36  			Reply(http.StatusNotFound)
    37  		gock.New(Docker.DaemonHost()).
    38  			Post("/v"+Docker.ClientVersion()+"/images/create").
    39  			MatchParam("fromImage", imageId).
    40  			MatchParam("tag", "latest").
    41  			Reply(http.StatusAccepted)
    42  		// Run test
    43  		assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId))
    44  		// Validate api
    45  		assert.Empty(t, apitest.ListUnmatchedRequests())
    46  	})
    47  
    48  	t.Run("does nothing if image exists", func(t *testing.T) {
    49  		// Setup mock docker
    50  		require.NoError(t, apitest.MockDocker(Docker))
    51  		defer gock.OffAll()
    52  		gock.New(Docker.DaemonHost()).
    53  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
    54  			Reply(http.StatusOK).
    55  			JSON(types.ImageInspect{})
    56  		// Run test
    57  		assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId))
    58  		// Validate api
    59  		assert.Empty(t, apitest.ListUnmatchedRequests())
    60  	})
    61  
    62  	t.Run("throws error if docker is unavailable", func(t *testing.T) {
    63  		// Setup mock docker
    64  		require.NoError(t, apitest.MockDocker(Docker))
    65  		defer gock.OffAll()
    66  		gock.New(Docker.DaemonHost()).
    67  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
    68  			Reply(http.StatusServiceUnavailable)
    69  		// Run test
    70  		assert.Error(t, DockerPullImageIfNotCached(context.Background(), imageId))
    71  		// Validate api
    72  		assert.Empty(t, apitest.ListUnmatchedRequests())
    73  	})
    74  
    75  	t.Run("throws error on failure to pull image", func(t *testing.T) {
    76  		timeUnit = time.Duration(0)
    77  		// Setup mock docker
    78  		require.NoError(t, apitest.MockDocker(Docker))
    79  		defer gock.OffAll()
    80  		gock.New(Docker.DaemonHost()).
    81  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
    82  			Reply(http.StatusNotFound)
    83  		// Total 3 tries
    84  		gock.New(Docker.DaemonHost()).
    85  			Post("/v"+Docker.ClientVersion()+"/images/create").
    86  			MatchParam("fromImage", imageId).
    87  			MatchParam("tag", "latest").
    88  			Reply(http.StatusServiceUnavailable)
    89  		gock.New(Docker.DaemonHost()).
    90  			Post("/v"+Docker.ClientVersion()+"/images/create").
    91  			MatchParam("fromImage", imageId).
    92  			MatchParam("tag", "latest").
    93  			Reply(http.StatusAccepted).
    94  			JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "toomanyrequests"}})
    95  		gock.New(Docker.DaemonHost()).
    96  			Post("/v"+Docker.ClientVersion()+"/images/create").
    97  			MatchParam("fromImage", imageId).
    98  			MatchParam("tag", "latest").
    99  			Reply(http.StatusAccepted).
   100  			JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "no space left on device"}})
   101  		// Run test
   102  		err := DockerPullImageIfNotCached(context.Background(), imageId)
   103  		// Validate api
   104  		assert.ErrorContains(t, err, "no space left on device")
   105  		assert.Empty(t, apitest.ListUnmatchedRequests())
   106  	})
   107  }
   108  
   109  func TestRunOnce(t *testing.T) {
   110  	viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io")
   111  
   112  	t.Run("runs once in container", func(t *testing.T) {
   113  		// Setup mock docker
   114  		require.NoError(t, apitest.MockDocker(Docker))
   115  		defer gock.OffAll()
   116  		apitest.MockDockerStart(Docker, imageId, containerId)
   117  		require.NoError(t, apitest.MockDockerLogs(Docker, containerId, "hello world"))
   118  		// Run test
   119  		out, err := DockerRunOnce(context.Background(), imageId, nil, nil)
   120  		assert.NoError(t, err)
   121  		// Validate api
   122  		assert.Equal(t, "hello world", out)
   123  		assert.Empty(t, apitest.ListUnmatchedRequests())
   124  	})
   125  
   126  	t.Run("throws error on container create", func(t *testing.T) {
   127  		// Setup mock docker
   128  		require.NoError(t, apitest.MockDocker(Docker))
   129  		defer gock.OffAll()
   130  		gock.New(Docker.DaemonHost()).
   131  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
   132  			Reply(http.StatusOK).
   133  			JSON(types.ImageInspect{})
   134  		gock.New(Docker.DaemonHost()).
   135  			Post("/v" + Docker.ClientVersion() + "/networks/create").
   136  			Reply(http.StatusCreated).
   137  			JSON(types.NetworkCreateResponse{})
   138  		gock.New(Docker.DaemonHost()).
   139  			Post("/v" + Docker.ClientVersion() + "/containers/create").
   140  			Reply(http.StatusServiceUnavailable)
   141  		// Run test
   142  		_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
   143  		assert.Error(t, err)
   144  		// Validate api
   145  		assert.Empty(t, apitest.ListUnmatchedRequests())
   146  	})
   147  
   148  	t.Run("throws error on container start", func(t *testing.T) {
   149  		// Setup mock docker
   150  		require.NoError(t, apitest.MockDocker(Docker))
   151  		defer gock.OffAll()
   152  		gock.New(Docker.DaemonHost()).
   153  			Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
   154  			Reply(http.StatusOK).
   155  			JSON(types.ImageInspect{})
   156  		gock.New(Docker.DaemonHost()).
   157  			Post("/v" + Docker.ClientVersion() + "/networks/create").
   158  			Reply(http.StatusCreated).
   159  			JSON(types.NetworkCreateResponse{})
   160  		gock.New(Docker.DaemonHost()).
   161  			Post("/v" + Docker.ClientVersion() + "/containers/create").
   162  			Reply(http.StatusOK).
   163  			JSON(container.ContainerCreateCreatedBody{ID: containerId})
   164  		gock.New(Docker.DaemonHost()).
   165  			Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/start").
   166  			Reply(http.StatusServiceUnavailable)
   167  		// Run test
   168  		_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
   169  		assert.Error(t, err)
   170  		// Validate api
   171  		assert.Empty(t, apitest.ListUnmatchedRequests())
   172  	})
   173  
   174  	t.Run("removes container on cancel", func(t *testing.T) {
   175  		// Setup mock docker
   176  		require.NoError(t, apitest.MockDocker(Docker))
   177  		defer gock.OffAll()
   178  		apitest.MockDockerStart(Docker, imageId, containerId)
   179  		gock.New(Docker.DaemonHost()).
   180  			Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
   181  			Reply(http.StatusOK).
   182  			SetHeader("Content-Type", "application/vnd.docker.raw-stream").
   183  			Delay(1 * time.Second)
   184  		ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(200*time.Millisecond))
   185  		defer cancel()
   186  		gock.New(Docker.DaemonHost()).
   187  			Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
   188  			Reply(http.StatusOK)
   189  		// Run test
   190  		_, err := DockerRunOnce(ctx, imageId, nil, nil)
   191  		assert.Error(t, err)
   192  		// Validate api
   193  		assert.Empty(t, apitest.ListUnmatchedRequests())
   194  	})
   195  
   196  	t.Run("throws error on failure to parse logs", func(t *testing.T) {
   197  		// Setup mock docker
   198  		require.NoError(t, apitest.MockDocker(Docker))
   199  		defer gock.OffAll()
   200  		apitest.MockDockerStart(Docker, imageId, containerId)
   201  		gock.New(Docker.DaemonHost()).
   202  			Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
   203  			Reply(http.StatusOK).
   204  			SetHeader("Content-Type", "application/vnd.docker.raw-stream").
   205  			BodyString("hello world")
   206  		gock.New(Docker.DaemonHost()).
   207  			Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
   208  			Reply(http.StatusOK)
   209  		// Run test
   210  		_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
   211  		assert.Error(t, err)
   212  		// Validate api
   213  		assert.Empty(t, apitest.ListUnmatchedRequests())
   214  	})
   215  
   216  	t.Run("throws error on failure to inspect", func(t *testing.T) {
   217  		// Setup mock docker
   218  		require.NoError(t, apitest.MockDocker(Docker))
   219  		defer gock.OffAll()
   220  		apitest.MockDockerStart(Docker, imageId, containerId)
   221  		// Setup docker style logs
   222  		var body bytes.Buffer
   223  		writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
   224  		_, err := writer.Write([]byte("hello world"))
   225  		require.NoError(t, err)
   226  		gock.New("http:///var/run/docker.sock").
   227  			Get("/v"+version+"/containers/"+containerId+"/logs").
   228  			Reply(http.StatusOK).
   229  			SetHeader("Content-Type", "application/vnd.docker.raw-stream").
   230  			Body(&body)
   231  		gock.New("http:///var/run/docker.sock").
   232  			Get("/v" + version + "/containers/" + containerId + "/json").
   233  			Reply(http.StatusServiceUnavailable)
   234  		gock.New(Docker.DaemonHost()).
   235  			Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
   236  			Reply(http.StatusOK)
   237  		// Run test
   238  		_, err = DockerRunOnce(context.Background(), imageId, nil, nil)
   239  		assert.Error(t, err)
   240  		// Validate api
   241  		assert.Empty(t, apitest.ListUnmatchedRequests())
   242  	})
   243  
   244  	t.Run("throws error on non-zero exit code", func(t *testing.T) {
   245  		// Setup mock docker
   246  		require.NoError(t, apitest.MockDocker(Docker))
   247  		defer gock.OffAll()
   248  		apitest.MockDockerStart(Docker, imageId, containerId)
   249  		// Setup docker style logs
   250  		var body bytes.Buffer
   251  		writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
   252  		_, err := writer.Write([]byte("hello world"))
   253  		require.NoError(t, err)
   254  		gock.New("http:///var/run/docker.sock").
   255  			Get("/v"+version+"/containers/"+containerId+"/logs").
   256  			Reply(http.StatusOK).
   257  			SetHeader("Content-Type", "application/vnd.docker.raw-stream").
   258  			Body(&body)
   259  		gock.New("http:///var/run/docker.sock").
   260  			Get("/v" + version + "/containers/" + containerId + "/json").
   261  			Reply(http.StatusOK).
   262  			JSON(types.ContainerJSONBase{State: &types.ContainerState{ExitCode: 1}})
   263  		gock.New(Docker.DaemonHost()).
   264  			Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
   265  			Reply(http.StatusOK)
   266  		// Run test
   267  		_, err = DockerRunOnce(context.Background(), imageId, nil, nil)
   268  		assert.Error(t, err)
   269  		// Validate api
   270  		assert.Empty(t, apitest.ListUnmatchedRequests())
   271  	})
   272  }
   273  
   274  func TestExecOnce(t *testing.T) {
   275  	t.Run("throws error on failure to exec", func(t *testing.T) {
   276  		// Setup mock server
   277  		require.NoError(t, apitest.MockDocker(Docker))
   278  		defer gock.OffAll()
   279  		gock.New(Docker.DaemonHost()).
   280  			Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec").
   281  			Reply(http.StatusServiceUnavailable)
   282  		// Run test
   283  		_, err := DockerExecOnce(context.Background(), containerId, nil, nil)
   284  		assert.Error(t, err)
   285  		// Validate api
   286  		assert.Empty(t, apitest.ListUnmatchedRequests())
   287  	})
   288  
   289  	t.Run("throws error on failure to hijack", func(t *testing.T) {
   290  		// Setup mock server
   291  		require.NoError(t, apitest.MockDocker(Docker))
   292  		defer gock.OffAll()
   293  		gock.New(Docker.DaemonHost()).
   294  			Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec").
   295  			Reply(http.StatusAccepted).
   296  			JSON(types.IDResponse{ID: "test-command"})
   297  		// Run test
   298  		_, err := DockerExecOnce(context.Background(), containerId, nil, nil)
   299  		assert.Error(t, err)
   300  		// Validate api
   301  		assert.Empty(t, apitest.ListUnmatchedRequests())
   302  	})
   303  
   304  	// TODO: mock tcp hijack
   305  }