Build apps with BuildKit so build stages stop piling up (closes #220)
Multi-stage app builds left an untagged image for every build stage after each deploy, and nothing could safely remove them. upaas now asks Docker for a BuildKit build, which keeps stages in Docker's build cache instead of as images; Docker limits and cleans that cache itself. The final image is still tagged `upaas-<app>:<N>`, so old-image cleanup is unchanged. BuildKit's progress messages are decoded and written to the deployment log as plain text. Deviation: adds `github.com/moby/buildkit` v0.16.0 (matching the pinned Docker client), added with `go get` since no make target adds dependencies. Judgement call: the cache limit is on by default only from Docker Engine 28.2; older engines need the `daemon.json` setting the README now names. Model: opus-5-5
This commit was merged in pull request #221.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package docker //nolint:testpackage // tests unexported regexps and Client struct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -11,8 +13,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
)
|
||||
|
||||
// mainBranch is the branch name used across validation tests.
|
||||
@@ -239,3 +243,74 @@ func TestPerformCloneRemovesContainerVolumes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerformBuildUsesBuildKit runs a build against a fake Docker API and
|
||||
// checks that it asks for BuildKit and that BuildKit's progress reaches the
|
||||
// build log as plain text.
|
||||
func TestPerformBuildUsesBuildKit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
status := controlapi.StatusResponse{Vertexes: []*controlapi.Vertex{{
|
||||
Digest: "sha256:1111",
|
||||
Name: "[build 2/2] RUN make",
|
||||
Started: &now,
|
||||
Completed: &now,
|
||||
}}}
|
||||
|
||||
data, err := status.Marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
trace, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/build"):
|
||||
if r.URL.Query().Get("version") != "2" {
|
||||
http.Error(w, "not a BuildKit build", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "{\"id\":\"moby.buildkit.trace\",\"aux\":%s}\n", trace)
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{"Id":"sha256:built"}`))
|
||||
}
|
||||
},
|
||||
))
|
||||
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()}
|
||||
|
||||
var buildLog bytes.Buffer
|
||||
|
||||
imageID, err := c.performBuild(t.Context(), BuildImageOptions{
|
||||
ContextDir: t.TempDir(),
|
||||
Tags: []string{"upaas-test:1"},
|
||||
LogWriter: &buildLog,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if imageID != "sha256:built" {
|
||||
t.Errorf("unexpected image ID %q", imageID)
|
||||
}
|
||||
|
||||
if !strings.Contains(buildLog.String(), "[build 2/2] RUN make") {
|
||||
t.Errorf("build log is missing the build step:\n%s", buildLog.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user