supabase-cli/internal/utils/docker_test.go

306 lines
11 KiB
Go

package utils
import (
"bytes"
"context"
"net/http"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
"github.com/h2non/gock"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/supabase/cli/internal/testing/apitest"
)
const (
containerId = "test-container"
imageId = "test-image"
)
func TestPullImage(t *testing.T) {
viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io")
t.Run("pulls image if missing", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusNotFound)
gock.New(Docker.DaemonHost()).
Post("/v"+Docker.ClientVersion()+"/images/create").
MatchParam("fromImage", imageId).
MatchParam("tag", "latest").
Reply(http.StatusAccepted)
// Run test
assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId))
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("does nothing if image exists", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusOK).
JSON(types.ImageInspect{})
// Run test
assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId))
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error if docker is unavailable", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusServiceUnavailable)
// Run test
assert.Error(t, DockerPullImageIfNotCached(context.Background(), imageId))
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on failure to pull image", func(t *testing.T) {
timeUnit = time.Duration(0)
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusNotFound)
// Total 3 tries
gock.New(Docker.DaemonHost()).
Post("/v"+Docker.ClientVersion()+"/images/create").
MatchParam("fromImage", imageId).
MatchParam("tag", "latest").
Reply(http.StatusServiceUnavailable)
gock.New(Docker.DaemonHost()).
Post("/v"+Docker.ClientVersion()+"/images/create").
MatchParam("fromImage", imageId).
MatchParam("tag", "latest").
Reply(http.StatusAccepted).
JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "toomanyrequests"}})
gock.New(Docker.DaemonHost()).
Post("/v"+Docker.ClientVersion()+"/images/create").
MatchParam("fromImage", imageId).
MatchParam("tag", "latest").
Reply(http.StatusAccepted).
JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "no space left on device"}})
// Run test
err := DockerPullImageIfNotCached(context.Background(), imageId)
// Validate api
assert.ErrorContains(t, err, "no space left on device")
assert.Empty(t, apitest.ListUnmatchedRequests())
})
}
func TestRunOnce(t *testing.T) {
viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io")
t.Run("runs once in container", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
apitest.MockDockerStart(Docker, imageId, containerId)
require.NoError(t, apitest.MockDockerLogs(Docker, containerId, "hello world"))
// Run test
out, err := DockerRunOnce(context.Background(), imageId, nil, nil)
assert.NoError(t, err)
// Validate api
assert.Equal(t, "hello world", out)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on container create", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusOK).
JSON(types.ImageInspect{})
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/networks/create").
Reply(http.StatusCreated).
JSON(network.CreateResponse{})
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/containers/create").
Reply(http.StatusServiceUnavailable)
// Run test
_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on container start", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json").
Reply(http.StatusOK).
JSON(types.ImageInspect{})
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/networks/create").
Reply(http.StatusCreated).
JSON(network.CreateResponse{})
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/containers/create").
Reply(http.StatusOK).
JSON(container.CreateResponse{ID: containerId})
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/start").
Reply(http.StatusServiceUnavailable)
// Run test
_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("removes container on cancel", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
apitest.MockDockerStart(Docker, imageId, containerId)
gock.New(Docker.DaemonHost()).
Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
Reply(http.StatusOK).
SetHeader("Content-Type", "application/vnd.docker.raw-stream").
Delay(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(200*time.Millisecond))
defer cancel()
gock.New(Docker.DaemonHost()).
Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
Reply(http.StatusOK)
// Run test
_, err := DockerRunOnce(ctx, imageId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on failure to parse logs", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
apitest.MockDockerStart(Docker, imageId, containerId)
gock.New(Docker.DaemonHost()).
Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
Reply(http.StatusOK).
SetHeader("Content-Type", "application/vnd.docker.raw-stream").
BodyString("hello world")
gock.New(Docker.DaemonHost()).
Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
Reply(http.StatusOK)
// Run test
_, err := DockerRunOnce(context.Background(), imageId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on failure to inspect", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
apitest.MockDockerStart(Docker, imageId, containerId)
// Setup docker style logs
var body bytes.Buffer
writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
_, err := writer.Write([]byte("hello world"))
require.NoError(t, err)
gock.New("http:///var/run/docker.sock").
Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
Reply(http.StatusOK).
SetHeader("Content-Type", "application/vnd.docker.raw-stream").
Body(&body)
gock.New("http:///var/run/docker.sock").
Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json").
Reply(http.StatusServiceUnavailable)
gock.New(Docker.DaemonHost()).
Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
Reply(http.StatusOK)
// Run test
_, err = DockerRunOnce(context.Background(), imageId, nil, nil)
assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version")
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on non-zero exit code", func(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
apitest.MockDockerStart(Docker, imageId, containerId)
// Setup docker style logs
var body bytes.Buffer
writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
_, err := writer.Write([]byte("hello world"))
require.NoError(t, err)
gock.New("http:///var/run/docker.sock").
Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs").
Reply(http.StatusOK).
SetHeader("Content-Type", "application/vnd.docker.raw-stream").
Body(&body)
gock.New("http:///var/run/docker.sock").
Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json").
Reply(http.StatusOK).
JSON(types.ContainerJSONBase{State: &types.ContainerState{ExitCode: 1}})
gock.New(Docker.DaemonHost()).
Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId).
Reply(http.StatusOK)
// Run test
_, err = DockerRunOnce(context.Background(), imageId, nil, nil)
assert.ErrorContains(t, err, "error running container: exit 1")
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
}
func TestExecOnce(t *testing.T) {
t.Run("throws error on failure to exec", func(t *testing.T) {
// Setup mock server
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec").
Reply(http.StatusServiceUnavailable)
// Run test
_, err := DockerExecOnce(context.Background(), containerId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on failure to hijack", func(t *testing.T) {
// Setup mock server
require.NoError(t, apitest.MockDocker(Docker))
defer gock.OffAll()
gock.New(Docker.DaemonHost()).
Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec").
Reply(http.StatusAccepted).
JSON(types.IDResponse{ID: "test-command"})
// Run test
_, err := DockerExecOnce(context.Background(), containerId, nil, nil)
assert.Error(t, err)
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
// TODO: mock tcp hijack
}