Attach a BuildKit session to builds and demultiplex container logs (closes #251)
Check / check (pull_request) Successful in 3m59s

Builds now attach a BuildKit session over the Docker API, as the docker
command line does, and pass its ID with the build. BuildKit fetches a base
image that is not on the host through that session; without one, Docker
Engine 27 failed the build with "no active sessions". The session is
closed when the build ends.

Container logs are now read with stdcopy, so the clone output in the build
log and the app logs no longer carry Docker's 8-byte frame headers, and the
commit is read from the clone output; the header in front of the COMMIT
line kept it from being found.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #256.
This commit is contained in:
2026-10-01 21:15:06 +02:00
committed by clawbot
parent d69f11f74c
commit 5f9948d7e2
3 changed files with 225 additions and 2 deletions
+52 -2
View File
@@ -3,12 +3,14 @@ package docker
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"os"
"path/filepath"
"regexp"
@@ -25,9 +27,11 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
controlapi "github.com/moby/buildkit/api/services/control"
buildkitclient "github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/util/progress/progressui"
"go.uber.org/fx"
@@ -388,12 +392,17 @@ func (c *Client) ContainerLogs(
}
}()
logs, err := io.ReadAll(reader)
// A container without a terminal, as all of upaas's are, sends its
// output in frames, each with a header naming stdout or stderr. Both
// go to one buffer, in the order they were written.
var logs bytes.Buffer
_, err = stdcopy.StdCopy(&logs, &logs, reader)
if err != nil {
return "", fmt.Errorf("failed to read container logs: %w", err)
}
return string(logs), nil
return logs.String(), nil
}
// IsContainerRunning checks if a container is running.
@@ -637,11 +646,24 @@ func (c *Client) performBuild(
}
}()
buildSession, err := c.startBuildSession(ctx)
if err != nil {
return "", err
}
defer func() {
closeErr := buildSession.Close()
if closeErr != nil {
c.log.Error("failed to close build session", "error", closeErr)
}
}()
// 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,
SessionID: buildSession.ID(),
Dockerfile: opts.DockerfilePath,
Tags: opts.Tags,
Remove: true,
@@ -677,6 +699,34 @@ func (c *Client) performBuild(
return "", nil
}
// startBuildSession attaches a BuildKit session to the daemon, as the docker
// command line does for a build. BuildKit asks the client, over the session,
// for registry access to fetch a base image that is not on the host; without
// a session, Docker Engine 27 fails the build with "no active sessions". The
// shared key is only used for a build context sent over the session; upaas
// sends the context with the build request. The caller closes the session.
func (c *Client) startBuildSession(ctx context.Context) (*session.Session, error) {
buildSession, err := session.NewSession(ctx, "")
if err != nil {
return nil, fmt.Errorf("failed to create build session: %w", err)
}
go func() {
runErr := buildSession.Run(ctx, func(
ctx context.Context,
proto string,
meta map[string][]string,
) (net.Conn, error) {
return c.docker.DialHijack(ctx, "/session", proto, meta)
})
if runErr != nil {
c.log.Error("build session failed", "error", runErr)
}
}()
return buildSession, nil
}
// scannerInitialBufferSize is the initial buffer size for the build log scanner.
const scannerInitialBufferSize = 64 * 1024 // 64KB