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 }