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