mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
Fixes https://gitea.com/gitea/runner/issues/1213 Fix the DooD regression that mounts `/var/run/docker.sock` as a directory. Keep the Docker proxy available through job and post steps. Clean stale resources before opening it, then remove containers before their networks and volumes during teardown. Use a unique filesystem probe and preserve socket ownership. Fall back to direct access when proxying is unsupported. Preserve exec output and clean up active streams and failed starts. Add a real Docker job test for mounted socket access, post steps and resource cleanup. --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1215 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
470 lines
18 KiB
Go
470 lines
18 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
|
|
|
package container
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/image"
|
|
"github.com/moby/moby/api/types/mount"
|
|
"github.com/moby/moby/api/types/volume"
|
|
mobyclient "github.com/moby/moby/client"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func (m *mockDockerClient) VolumeList(ctx context.Context, opts mobyclient.VolumeListOptions) (mobyclient.VolumeListResult, error) {
|
|
args := m.Called(ctx, opts)
|
|
return args.Get(0).(mobyclient.VolumeListResult), args.Error(1)
|
|
}
|
|
|
|
func (m *mockDockerClient) VolumeRemove(ctx context.Context, id string, opts mobyclient.VolumeRemoveOptions) (mobyclient.VolumeRemoveResult, error) {
|
|
args := m.Called(ctx, id, opts)
|
|
return args.Get(0).(mobyclient.VolumeRemoveResult), args.Error(1)
|
|
}
|
|
|
|
// unix socket paths are limited to about a hundred bytes, which the default macOS TMPDIR exceeds
|
|
func shortTempDir(t testing.TB) string {
|
|
t.Helper()
|
|
t.Setenv("TMPDIR", "/tmp")
|
|
return t.TempDir()
|
|
}
|
|
|
|
func daemonSocketPath(t testing.TB, cli mobyclient.APIClient) string {
|
|
t.Helper()
|
|
host := cli.DaemonHost()
|
|
if !strings.HasPrefix(host, "unix://") {
|
|
t.Skipf("skipping: daemon at %s is not a unix socket", host)
|
|
}
|
|
return strings.TrimPrefix(host, "unix://")
|
|
}
|
|
|
|
func TestDockerProxy(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Unix socket ownership is unavailable on Windows")
|
|
}
|
|
bodies := make(chan []byte, 8)
|
|
createStarted, releaseCreate := make(chan struct{}), make(chan struct{})
|
|
streamsDone := make(chan struct{}, 2)
|
|
daemonSocket := filepath.Join(shortTempDir(t), "d.sock")
|
|
listener, err := net.Listen("unix", daemonSocket)
|
|
require.NoError(t, err)
|
|
daemon := &http.Server{ReadHeaderTimeout: time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/create"):
|
|
body, _ := io.ReadAll(r.Body)
|
|
if r.URL.RawQuery == "wait" {
|
|
close(createStarted)
|
|
<-releaseCreate
|
|
}
|
|
bodies <- body
|
|
w.WriteHeader(http.StatusCreated)
|
|
case r.URL.Path == "/exec/detached/start" || r.URL.Path == "/exec/error/start":
|
|
if r.URL.Path == "/exec/error/start" {
|
|
w.Header().Set("Content-Type", "application/vnd.docker.raw-stream")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
_, _ = w.Write([]byte("ordinary"))
|
|
case rawStreamPath.MatchString(r.URL.Path) || r.URL.Path == "/session":
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
|
conn, buffered, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
if r.Header.Get("Upgrade") != "" {
|
|
_, _ = conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\nready\n"))
|
|
} else {
|
|
_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\nready\n"))
|
|
}
|
|
input, _ := io.ReadAll(buffered)
|
|
_, _ = conn.Write(append([]byte("final:"), input...))
|
|
streamsDone <- struct{}{}
|
|
default:
|
|
w.Header().Set("Api-Version", "1.47")
|
|
_, _ = w.Write([]byte("OK " + r.Method + " " + r.URL.Path))
|
|
}
|
|
})}
|
|
go func() { _ = daemon.Serve(listener) }()
|
|
t.Cleanup(func() { _ = daemon.Close() })
|
|
proxy, err := StartDockerProxy(daemonSocket, shortTempDir(t), "job-1")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = proxy.Close(context.Background()) })
|
|
client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{}).DialContext(ctx, "unix", proxy.Socket)
|
|
}}}
|
|
t.Cleanup(client.CloseIdleConnections)
|
|
dialProxy := func(t *testing.T) *net.UnixConn {
|
|
t.Helper()
|
|
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: proxy.Socket, Net: "unix"})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
|
|
return conn
|
|
}
|
|
startStream := func(t *testing.T, path, upgrade string) (*net.UnixConn, *bufio.Reader) {
|
|
t.Helper()
|
|
conn := dialProxy(t)
|
|
_, err := fmt.Fprintf(conn, "POST %s HTTP/1.1\r\nHost: docker\r\n%sContent-Length: 2\r\n\r\n{}input", path, upgrade)
|
|
require.NoError(t, err)
|
|
reader := bufio.NewReader(conn)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = conn.Close()
|
|
_ = resp.Body.Close()
|
|
})
|
|
prefix, err := reader.ReadString('\n')
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ready\n", prefix)
|
|
return conn, reader
|
|
}
|
|
|
|
t.Run("labels creates", func(t *testing.T) {
|
|
for _, testCase := range []struct{ path, body, want string }{
|
|
{
|
|
"/v1.47/containers/create", `{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job":"other"}}`,
|
|
`{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
{
|
|
"/networks/create", `{"Name":"n"}`,
|
|
`{"Name":"n","Labels":{"com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
{
|
|
"/volumes/create", `{"Name":"v","labels":null}`,
|
|
`{"Name":"v","Labels":{"com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
{
|
|
"/volumes/create", "",
|
|
`{"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
{
|
|
"/volumes/create", "null",
|
|
`{"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
{
|
|
"/volumes/create", `{"Labels":{"discard":"1"},"labels":null,"LABELS":{"own":"1"},"LABELS":{"extra":"2"}}`,
|
|
`{"Labels":{"own":"1","extra":"2","com.gitea.runner.job":"job-1"}}`,
|
|
},
|
|
} {
|
|
resp, err := client.Post("http://docker"+testCase.path, "application/json", strings.NewReader(testCase.body))
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
assert.JSONEq(t, testCase.want, string(<-bodies), testCase.body)
|
|
}
|
|
})
|
|
|
|
t.Run("passes other requests through", func(t *testing.T) {
|
|
resp, err := client.Get("http://docker/v1.47/_ping")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
assert.Equal(t, "1.47", resp.Header.Get("Api-Version"))
|
|
assert.Equal(t, "OK GET /v1.47/_ping", string(body))
|
|
})
|
|
|
|
t.Run("tunnels raw streams through stdin EOF", func(t *testing.T) {
|
|
for _, stream := range []struct{ path, upgrade string }{
|
|
{path: "/v1.47/exec/abc/start"},
|
|
{path: "/containers/abc/attach", upgrade: "Connection: Upgrade\r\nUpgrade: tcp\r\n"},
|
|
} {
|
|
conn, reader := startStream(t, stream.path, stream.upgrade)
|
|
require.NoError(t, conn.CloseWrite())
|
|
output, err := io.ReadAll(reader)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "final:input", string(output))
|
|
<-streamsDone
|
|
}
|
|
})
|
|
|
|
t.Run("detached and error responses retain keepalive label injection", func(t *testing.T) {
|
|
conn := dialProxy(t)
|
|
reader := bufio.NewReader(conn)
|
|
for endpoint, status := range map[string]int{"detached": http.StatusOK, "error": http.StatusBadRequest} {
|
|
_, err := fmt.Fprintf(conn, "POST /exec/%s/start HTTP/1.1\r\nHost: docker\r\nContent-Length: 2\r\n\r\n{}", endpoint)
|
|
require.NoError(t, err)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
assert.Equal(t, status, resp.StatusCode)
|
|
assert.Equal(t, "ordinary", string(body))
|
|
_, err = io.WriteString(conn, "POST /volumes/create HTTP/1.1\r\nHost: docker\r\nContent-Length: 0\r\n\r\n")
|
|
require.NoError(t, err)
|
|
resp, err = http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job":"job-1"}}`, string(<-bodies))
|
|
}
|
|
})
|
|
|
|
t.Run("close joins streams and admitted create", func(t *testing.T) {
|
|
t.Cleanup(func() { close(releaseCreate) })
|
|
_, raw := startStream(t, "/exec/live/start", "")
|
|
_, upgraded := startStream(t, "/session", "Connection: Upgrade\r\nUpgrade: tcp\r\n")
|
|
creator := dialProxy(t)
|
|
_, err := io.WriteString(creator, "POST /volumes/create?wait HTTP/1.1\r\nHost: docker\r\nContent-Length: 2\r\n\r\n{}")
|
|
require.NoError(t, err)
|
|
<-createStarted
|
|
require.NoError(t, creator.CloseWrite())
|
|
closed := make(chan error, 1)
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
go func() { closed <- proxy.Close(ctx) }()
|
|
for _, reader := range []*bufio.Reader{raw, upgraded} {
|
|
_, err := reader.ReadByte()
|
|
require.ErrorIs(t, err, io.EOF)
|
|
<-streamsDone
|
|
}
|
|
select {
|
|
case err := <-closed:
|
|
t.Fatalf("Close returned before admitted create settled: %v", err)
|
|
default:
|
|
}
|
|
releaseCreate <- struct{}{}
|
|
resp, err := http.ReadResponse(bufio.NewReader(creator), nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job":"job-1"}}`, string(<-bodies))
|
|
require.NoError(t, <-closed)
|
|
})
|
|
}
|
|
|
|
func TestRemoveLabelledRemovesContainersNetworksAndVolumes(t *testing.T) {
|
|
containerFailure := errors.New("container removal failed")
|
|
listFailure := errors.New("listing failed")
|
|
volumeFailure := errors.New("volume removal failed")
|
|
ctx := context.Background()
|
|
filters := make(mobyclient.Filters).Add("label", jobLabel+"=job-1")
|
|
cli := &mockDockerClient{}
|
|
cli.On("ContainerList", ctx, mobyclient.ContainerListOptions{All: true, Filters: filters}).
|
|
Return(mobyclient.ContainerListResult{Items: []container.Summary{{ID: "c1", Names: []string{"/app"}}}}, nil).Once()
|
|
cli.On("ContainerKill", ctx, "c1", mock.Anything).Return(mobyclient.ContainerKillResult{}, nil).Once()
|
|
cli.On("ContainerRemove", ctx, "c1", mobyclient.ContainerRemoveOptions{RemoveVolumes: true, Force: true}).
|
|
Return(mobyclient.ContainerRemoveResult{}, containerFailure).Once()
|
|
cli.On("NetworkList", ctx, mobyclient.NetworkListOptions{Filters: filters}).Return(mobyclient.NetworkListResult{}, listFailure).Once()
|
|
cli.On("VolumeList", ctx, mobyclient.VolumeListOptions{Filters: filters}).
|
|
Return(mobyclient.VolumeListResult{Items: []volume.Volume{{Name: "app_data"}}}, nil).Once()
|
|
cli.On("VolumeRemove", ctx, "app_data", mobyclient.VolumeRemoveOptions{}).Return(mobyclient.VolumeRemoveResult{}, volumeFailure).Once()
|
|
|
|
err := removeLabelled(ctx, cli, "job-1")
|
|
require.ErrorIs(t, err, containerFailure)
|
|
require.ErrorIs(t, err, listFailure)
|
|
require.ErrorIs(t, err, volumeFailure)
|
|
require.ErrorContains(t, err, "failed to remove container c1")
|
|
require.ErrorContains(t, err, "failed to remove volume app_data")
|
|
cli.AssertExpectations(t)
|
|
}
|
|
|
|
func TestDockerProxyWithDaemon(t *testing.T) {
|
|
requireDocker(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, NewDockerPullExecutor(NewDockerPullExecutorInput{Image: "alpine"})(ctx))
|
|
direct, err := GetDockerClient(ctx)
|
|
require.NoError(t, err)
|
|
defer direct.Close()
|
|
dir := shortTempDir(t)
|
|
seen, err := daemonSeesDir(ctx, direct, dir)
|
|
require.NoError(t, err)
|
|
t.Logf("daemon sees the runner's filesystem: %v", seen)
|
|
|
|
job := "proxy-test-" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
proxy, err := StartDockerProxy(daemonSocketPath(t, direct), dir, job)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = proxy.Close(context.Background()) })
|
|
viaProxy, err := mobyclient.New(mobyclient.WithHost("unix://" + proxy.Socket))
|
|
require.NoError(t, err)
|
|
defer viaProxy.Close()
|
|
|
|
created, err := viaProxy.ContainerCreate(ctx, mobyclient.ContainerCreateOptions{
|
|
Config: &container.Config{Image: "alpine", Cmd: []string{"sleep", "300"}, Labels: map[string]string{"own": "1"}},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _, _ = direct.ContainerRemove(ctx, created.ID, mobyclient.ContainerRemoveOptions{Force: true}) })
|
|
_, err = viaProxy.ContainerStart(ctx, created.ID, mobyclient.ContainerStartOptions{})
|
|
require.NoError(t, err)
|
|
net, err := viaProxy.NetworkCreate(ctx, job, mobyclient.NetworkCreateOptions{})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _, _ = direct.NetworkRemove(ctx, net.ID, mobyclient.NetworkRemoveOptions{}) })
|
|
_, err = viaProxy.VolumeCreate(ctx, mobyclient.VolumeCreateOptions{Name: job})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _, _ = direct.VolumeRemove(ctx, job, mobyclient.VolumeRemoveOptions{Force: true}) })
|
|
|
|
inspected, err := direct.ContainerInspect(ctx, created.ID, mobyclient.ContainerInspectOptions{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"own": "1", jobLabel: job}, inspected.Container.Config.Labels)
|
|
vol, err := direct.VolumeInspect(ctx, job, mobyclient.VolumeInspectOptions{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, job, vol.Volume.Labels[jobLabel])
|
|
|
|
exec, err := viaProxy.ExecCreate(ctx, created.ID, mobyclient.ExecCreateOptions{Cmd: []string{"cat"}, AttachStdin: true, AttachStdout: true, TTY: true})
|
|
require.NoError(t, err)
|
|
attached, err := viaProxy.ExecAttach(ctx, exec.ID, mobyclient.ExecAttachOptions{TTY: true})
|
|
require.NoError(t, err)
|
|
_, err = attached.Conn.Write([]byte("hello\n"))
|
|
require.NoError(t, err)
|
|
echoed, err := attached.Reader.ReadString('\n')
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello\r\n", echoed)
|
|
attached.Close()
|
|
|
|
require.NoError(t, proxy.Close(ctx))
|
|
require.NoError(t, RemoveDockerJobResources(ctx, job))
|
|
_, err = direct.ContainerInspect(ctx, created.ID, mobyclient.ContainerInspectOptions{})
|
|
assert.True(t, cerrdefs.IsNotFound(err))
|
|
_, err = direct.NetworkInspect(ctx, net.ID, mobyclient.NetworkInspectOptions{})
|
|
assert.True(t, cerrdefs.IsNotFound(err))
|
|
_, err = direct.VolumeInspect(ctx, job, mobyclient.VolumeInspectOptions{})
|
|
assert.True(t, cerrdefs.IsNotFound(err))
|
|
}
|
|
|
|
func BenchmarkDockerProxy(b *testing.B) {
|
|
ctx := context.Background()
|
|
direct, err := GetDockerClient(ctx)
|
|
require.NoError(b, err)
|
|
defer direct.Close()
|
|
if _, err := direct.Ping(ctx, mobyclient.PingOptions{}); err != nil {
|
|
b.Skipf("docker daemon unreachable: %v", err)
|
|
}
|
|
proxy, err := StartDockerProxy(daemonSocketPath(b, direct), shortTempDir(b), "bench")
|
|
require.NoError(b, err)
|
|
defer func() { _ = proxy.Close(ctx) }()
|
|
viaProxy, err := mobyclient.New(mobyclient.WithHost("unix://" + proxy.Socket))
|
|
require.NoError(b, err)
|
|
defer viaProxy.Close()
|
|
|
|
require.NoError(b, NewDockerPullExecutor(NewDockerPullExecutorInput{Image: "alpine"})(ctx))
|
|
created, err := direct.ContainerCreate(ctx, mobyclient.ContainerCreateOptions{Config: &container.Config{Image: "alpine", Cmd: []string{"sleep", "600"}}})
|
|
require.NoError(b, err)
|
|
defer func() { _, _ = direct.ContainerRemove(ctx, created.ID, mobyclient.ContainerRemoveOptions{Force: true}) }()
|
|
_, err = direct.ContainerStart(ctx, created.ID, mobyclient.ContainerStartOptions{})
|
|
require.NoError(b, err)
|
|
|
|
var archive bytes.Buffer
|
|
writer := tar.NewWriter(&archive)
|
|
payload := make([]byte, 64<<20)
|
|
require.NoError(b, writer.WriteHeader(&tar.Header{Name: "blob", Mode: 0o600, Size: int64(len(payload))}))
|
|
_, _ = writer.Write(payload)
|
|
require.NoError(b, writer.Close())
|
|
|
|
for name, cli := range map[string]mobyclient.APIClient{"direct": direct, "proxy": viaProxy} {
|
|
b.Run("ping/"+name, func(b *testing.B) {
|
|
for b.Loop() {
|
|
if _, err := cli.Ping(ctx, mobyclient.PingOptions{}); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
b.Run("copy64MiB/"+name, func(b *testing.B) {
|
|
b.SetBytes(int64(archive.Len()))
|
|
for b.Loop() {
|
|
_, err := cli.CopyToContainer(ctx, created.ID, mobyclient.CopyToContainerOptions{DestinationPath: "/tmp", Content: bytes.NewReader(archive.Bytes())})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type probeClient struct {
|
|
mobyclient.APIClient
|
|
create func(mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error)
|
|
remove func(context.Context, string, mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error)
|
|
}
|
|
|
|
func (c *probeClient) ImageList(context.Context, mobyclient.ImageListOptions) (mobyclient.ImageListResult, error) {
|
|
return mobyclient.ImageListResult{Items: []image.Summary{{ID: "probe-image"}}}, nil
|
|
}
|
|
|
|
func (c *probeClient) ContainerCreate(_ context.Context, opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
|
return c.create(opts)
|
|
}
|
|
|
|
func (c *probeClient) ContainerRemove(ctx context.Context, id string, opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
|
return c.remove(ctx, id, opts)
|
|
}
|
|
|
|
func TestDaemonSeesDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
markers := make(map[string]bool)
|
|
for _, testCase := range []struct {
|
|
name string
|
|
private bool
|
|
removeErr error
|
|
}{
|
|
{name: "cleanup after cancellation"},
|
|
{name: "private filesystem with stale marker", private: true},
|
|
{name: "cleanup error after cancellation", removeErr: errors.New("cleanup failed")},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
daemonDir := dir
|
|
if testCase.private {
|
|
daemonDir = t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(daemonDir, "probe"), []byte("stale"), 0o600))
|
|
}
|
|
removed := false
|
|
cli := &probeClient{
|
|
create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
|
require.Len(t, opts.HostConfig.Mounts, 1)
|
|
marker := opts.HostConfig.Mounts[0].Source
|
|
assert.Equal(t, mount.Mount{Type: mount.TypeBind, Source: marker, Target: "/gitea-runner-probe", ReadOnly: true}, opts.HostConfig.Mounts[0])
|
|
assert.False(t, markers[marker])
|
|
markers[marker] = true
|
|
info, err := os.Stat(marker)
|
|
require.NoError(t, err)
|
|
assert.True(t, info.Mode().IsRegular())
|
|
cancel()
|
|
if _, err := os.Stat(filepath.Join(daemonDir, filepath.Base(marker))); errors.Is(err, os.ErrNotExist) {
|
|
return mobyclient.ContainerCreateResult{}, cerrdefs.ErrInvalidArgument
|
|
}
|
|
return mobyclient.ContainerCreateResult{ID: "probe"}, nil
|
|
},
|
|
remove: func(ctx context.Context, id string, opts mobyclient.ContainerRemoveOptions) (mobyclient.ContainerRemoveResult, error) {
|
|
removed = true
|
|
assert.Equal(t, "probe", id)
|
|
assert.Equal(t, mobyclient.ContainerRemoveOptions{Force: true, RemoveVolumes: true}, opts)
|
|
require.NoError(t, ctx.Err())
|
|
_, bounded := ctx.Deadline()
|
|
assert.True(t, bounded)
|
|
return mobyclient.ContainerRemoveResult{}, testCase.removeErr
|
|
},
|
|
}
|
|
seen, err := daemonSeesDir(ctx, cli, dir)
|
|
require.ErrorIs(t, err, testCase.removeErr)
|
|
assert.Equal(t, !testCase.private && testCase.removeErr == nil, seen)
|
|
assert.Equal(t, !testCase.private, removed)
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
})
|
|
}
|
|
}
|