Remove the git clone container's anonymous volume with it (closes #215)

The pinned alpine/git image declares a volume at /git, so Docker gives
every clone container an anonymous volume. The container was removed
without its volumes, leaving one volume behind per deploy. The removal
now also removes the container's anonymous volumes, on success and on
failure. A test runs a clone against a fake Docker API and checks the
removal asks for volumes to be removed.

Model: opus-5-5
This commit is contained in:
2026-09-23 09:45:07 +00:00
parent ddcd179841
commit a44d4a2647
3 changed files with 78 additions and 1 deletions
+72
View File
@@ -2,8 +2,16 @@ package docker //nolint:testpackage // tests unexported regexps and Client struc
import (
"errors"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
"github.com/docker/docker/client"
)
// mainBranch is the branch name used across validation tests.
@@ -149,3 +157,67 @@ func TestCloneRepoRejectsInjection(t *testing.T) {
})
}
}
// TestPerformCloneRemovesContainerVolumes runs a clone against a fake Docker
// API and checks that the clone container is removed together with its
// anonymous volumes, whether the clone succeeds or fails.
func TestPerformCloneRemovesContainerVolumes(t *testing.T) {
t.Parallel()
for _, exitCode := range []int{0, 1} {
t.Run(fmt.Sprintf("exit code %d", exitCode), func(t *testing.T) {
t.Parallel()
removeQuery := make(chan url.Values, 1)
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodDelete:
removeQuery <- r.URL.Query()
case strings.HasSuffix(r.URL.Path, "/containers/create"):
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
case strings.HasSuffix(r.URL.Path, "/wait"):
_, _ = fmt.Fprintf(w, `{"StatusCode":%d}`, exitCode)
default:
_, _ = w.Write([]byte(`{}`))
}
},
))
t.Cleanup(srv.Close)
dockerAPI, err := client.NewClientWithOpts(
client.WithHost("tcp://" + srv.Listener.Addr().String()),
)
if err != nil {
t.Fatal(err)
}
c := &Client{docker: dockerAPI, log: slog.Default()}
dir := t.TempDir()
cfg := &cloneConfig{
repoURL: "git@example.com:repo.git",
branch: mainBranch,
sshPrivateKey: "fake-key",
containerDir: filepath.Join(dir, "repo"),
hostDir: filepath.Join(dir, "repo"),
keyFile: filepath.Join(dir, "deploy_key"),
hostKeyFile: filepath.Join(dir, "deploy_key"),
}
_, _ = c.performClone(t.Context(), cfg)
select {
case query := <-removeQuery:
if query.Get("v") != "1" {
t.Errorf("clone container removed without its volumes: %v", query)
}
default:
t.Error("clone container was not removed")
}
})
}
}