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 }