306 lines
11 KiB
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
|
|
}
|