Build apps with BuildKit so build stages stop piling up (closes #220)
Check / check (pull_request) Skipped
Check / check (pull_request) Skipped
The classic builder left an untagged image for every step of every stage of a multi-stage build. BuildKit keeps stages in Docker's build cache, which Docker keeps under a size limit on its own. BuildKit reports progress as encoded trace messages, so these are decoded with BuildKit's own library and written to the deployment log as plain text. The README notes that engines older than 28.2 need build cache cleanup turned on in daemon.json. Model: opus-5-5
This commit is contained in:
+61
-15
@@ -4,6 +4,7 @@ package docker
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -22,7 +23,11 @@ import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/go-connections/nat"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
buildkitclient "github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"sneak.berlin/go/upaas/internal/config"
|
||||
@@ -603,8 +608,11 @@ func (c *Client) performBuild(
|
||||
}
|
||||
}()
|
||||
|
||||
// Build image
|
||||
// Build with BuildKit: the stages of a multi-stage build are kept in
|
||||
// its build cache, which Docker limits on its own, instead of being
|
||||
// left behind as untagged images.
|
||||
resp, err := c.docker.ImageBuild(ctx, tarArchive, dockertypes.ImageBuildOptions{
|
||||
Version: dockertypes.BuilderBuildKit,
|
||||
Dockerfile: opts.DockerfilePath,
|
||||
Tags: opts.Tags,
|
||||
Remove: true,
|
||||
@@ -622,7 +630,7 @@ func (c *Client) performBuild(
|
||||
}()
|
||||
|
||||
// Stream build output line by line for real-time log updates
|
||||
err = c.streamBuildOutput(resp.Body, opts.LogWriter)
|
||||
err = c.streamBuildOutput(ctx, resp.Body, opts.LogWriter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -647,28 +655,66 @@ const scannerInitialBufferSize = 64 * 1024 // 64KB
|
||||
// (base64 layers can be large).
|
||||
const scannerMaxBufferSize = 1024 * 1024 // 1MB
|
||||
|
||||
// streamBuildOutput reads Docker build output line by line and writes to
|
||||
// stdout and optional log writer. Docker sends newline-delimited JSON, so
|
||||
// reading line by line ensures each log entry is written immediately.
|
||||
func (c *Client) streamBuildOutput(body io.Reader, logWriter io.Writer) error {
|
||||
// streamBuildOutput reads Docker build output line by line and writes it to
|
||||
// stdout and the optional log writer as it arrives. Docker sends
|
||||
// newline-delimited JSON. BuildKit's progress arrives encoded in
|
||||
// "moby.buildkit.trace" messages; these are decoded and written as plain
|
||||
// text, as "docker build --progress=plain" shows it. Other lines, such as
|
||||
// build errors, are written unchanged.
|
||||
func (c *Client) streamBuildOutput(
|
||||
ctx context.Context,
|
||||
body io.Reader,
|
||||
logWriter io.Writer,
|
||||
) error {
|
||||
out := io.Writer(os.Stdout)
|
||||
if logWriter != nil {
|
||||
out = io.MultiWriter(os.Stdout, logWriter)
|
||||
}
|
||||
|
||||
display, err := progressui.NewDisplay(out, progressui.PlainMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create build progress display: %w", err)
|
||||
}
|
||||
|
||||
statuses := make(chan *buildkitclient.SolveStatus)
|
||||
displayDone := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(displayDone)
|
||||
// The display must keep reading until statuses is closed, even
|
||||
// after ctx is cancelled, or the loop below would block.
|
||||
_, _ = display.UpdateFrom(context.WithoutCancel(ctx), statuses)
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
buf := make([]byte, 0, scannerInitialBufferSize)
|
||||
scanner.Buffer(buf, scannerMaxBufferSize)
|
||||
|
||||
newline := []byte{'\n'}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// Write to stdout
|
||||
_, _ = os.Stdout.Write(line)
|
||||
_, _ = os.Stdout.Write(newline)
|
||||
// Write to log writer if provided
|
||||
if logWriter != nil {
|
||||
_, _ = logWriter.Write(line)
|
||||
_, _ = logWriter.Write(newline)
|
||||
|
||||
var msg jsonmessage.JSONMessage
|
||||
|
||||
err = json.Unmarshal(line, &msg)
|
||||
if err == nil && msg.ID == "moby.buildkit.trace" && msg.Aux != nil {
|
||||
var data []byte
|
||||
|
||||
var status controlapi.StatusResponse
|
||||
|
||||
if json.Unmarshal(*msg.Aux, &data) == nil && status.Unmarshal(data) == nil {
|
||||
statuses <- buildkitclient.NewSolveStatus(&status)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// One write per line, so it is not split by the display's output.
|
||||
_, _ = fmt.Fprintf(out, "%s\n", line)
|
||||
}
|
||||
|
||||
close(statuses)
|
||||
<-displayDone
|
||||
|
||||
scanErr := scanner.Err()
|
||||
if scanErr != nil {
|
||||
return fmt.Errorf("failed to read build output: %w", scanErr)
|
||||
|
||||
Reference in New Issue
Block a user