Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: Allow test suite to run on mac #248

Merged
merged 3 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/windows/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ const (

var (
defaultPlatform = ocispecs.Platform{
OS: outputKey,
OS: outputKey,
// NOTE: Windows is (currently) only supported on amd64.
// Making this use runtime.GOARCH so that builds are more explicitly and not suprising.
// If/when Windows is supported on another platform (ie arm64) this will work as expected.
// Until then, if someone really wants to build an amd64 image from arm64 they'll need to set the platform explicitly in the build request.
Architecture: runtime.GOARCH,
}
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/windows/handle_zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func buildBinaries(spec *dalec.Spec, worker llb.State, sOpt dalec.SourceOpts, ta

sources, err := specToSourcesLLB(worker, spec, sOpt)
if err != nil {
return llb.Scratch(), err
return llb.Scratch(), errors.Wrap(err, "could not generate sources")
}

patched := dalec.PatchSources(worker, spec, sources)
Expand Down
3 changes: 0 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ toolchain go1.21.0

require (
github.com/containerd/containerd v1.7.13
github.com/cpuguy83/dockercfg v0.3.1
github.com/cpuguy83/go-docker v0.3.0
github.com/cpuguy83/go-docker/buildkitopt v0.1.2
github.com/goccy/go-yaml v1.11.3
github.com/google/go-cmp v0.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,6 @@ github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtO
github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak=
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-docker v0.3.0 h1:O88rocdycYvY+pUYYp0i1rRDANXHurNir3VE0F/PH3g=
github.com/cpuguy83/go-docker v0.3.0/go.mod h1:R2HgB/m54W+2dhYc70Xm78yS6o775SfN09bGIPSfQZQ=
github.com/cpuguy83/go-docker/buildkitopt v0.1.2 h1:ikh1gGd33k+SIUYvz86cN0kq9p1KhIJox5tIXcemv74=
github.com/cpuguy83/go-docker/buildkitopt v0.1.2/go.mod h1:BpQa6UGlRuOHv/oTI83l1h+14DYWwh0eMaDaC9essbg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/phony.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ func PhonyFrontend(ctx context.Context, gwc gwclient.Client) (*gwclient.Result,
return nil, err
}

st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)).
p := llb.Platform(dc.BuildPlatforms[0])
st := llb.Image("golang:1.22", llb.WithMetaResolver(gwc), p).
Run(
llb.Args([]string{"go", "build", "-o=/build/out", "./test/fixtures/phony"}),
llb.AddEnv("CGO_ENABLED", "0"),
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func PhonySigner(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, er
return nil, err
}

st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)).
p := llb.Platform(dc.BuildPlatforms[0])
st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc), p).
Run(
llb.Args([]string{"go", "build", "-o=/build/out", "./test/fixtures/signer"}),
llb.AddEnv("CGO_ENABLED", "0"),
Expand Down
254 changes: 120 additions & 134 deletions test/testenv/buildx.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
package testenv

import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"

"github.com/cpuguy83/dockercfg"
"github.com/cpuguy83/go-docker"
"github.com/cpuguy83/go-docker/buildkitopt"
"github.com/cpuguy83/go-docker/container"
"github.com/cpuguy83/go-docker/transport"
"github.com/moby/buildkit/client"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
pkgerrors "github.com/pkg/errors"

gwclient "github.com/moby/buildkit/frontend/gateway/client"
)

type BuildxEnv struct {
Expand All @@ -30,9 +27,6 @@ type BuildxEnv struct {
mu sync.Mutex
client *client.Client

ctr *container.Container
docker *docker.Client

supportedOnce sync.Once
supportedErr error

Expand All @@ -57,161 +51,153 @@ func (b *BuildxEnv) Load(ctx context.Context, id string, f gwclient.BuildFunc) e
return nil
}

// bootstrap is ultimately responsible for creating a buildkit client.
// It looks like the buildx config on the client (typically in $HOME/.docker/buildx) to determine how to connect to the configured buildkit.
func (b *BuildxEnv) bootstrap(ctx context.Context) (retErr error) {
if b.client != nil {
return nil
func (b *BuildxEnv) version(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "docker", "buildx", "version")
out, err := cmd.CombinedOutput()
if err != nil {
return "", pkgerrors.Wrap(err, string(out))
}

defer func() {
if retErr != nil {
return
}
fields := strings.Fields(string(out))

b.supportedOnce.Do(func() {
info, err := b.client.Info(ctx)
if err != nil {
b.supportedErr = err
return
}
if len(fields) != 3 {
return "", errors.New("could not determine buildx version")
}

if !supportsFrontendAsInput(info) {
b.supportedErr = fmt.Errorf("buildkit version not supported: min version is v%s, got: %s", minVersion, info.BuildkitVersion.Version)
}
})
if b.supportedErr != nil {
b.client.Close()
b.client = nil
retErr = b.supportedErr
}
}()
ver, _, _ := strings.Cut(strings.TrimPrefix(fields[1], "v"), "-")
if strings.Count(ver, ".") < 2 {
return "", fmt.Errorf("unexpected version format: %s", ver)
}
return ver, nil
}

p, err := dockercfg.ConfigPath()
func (b *BuildxEnv) supportsDialStdio(ctx context.Context) (bool, error) {
ver, err := b.version(ctx)
if err != nil {
return err
return false, err
}

if out, err := exec.Command("docker", "buildx", "inspect", "--bootstrap", b.builder).CombinedOutput(); err != nil {
return pkgerrors.Wrapf(err, "failed to bootstrap builder: %s", out)
majorStr, other, _ := strings.Cut(ver, ".")
major, err := strconv.Atoi(majorStr)
if err != nil {
return false, pkgerrors.Wrapf(err, "could not parse major version number: %s", ver)
}
if major > 0 {
return true, nil
}

minorStr, _, _ := strings.Cut(other, ".")
minor, err := strconv.Atoi(minorStr)
if err != nil {
return false, pkgerrors.Wrapf(err, "could not parse major version number: %s", ver)
}
return minor >= 13, nil
}

configBase := filepath.Join(filepath.Dir(p), "buildx")
var errDialStdioNotSupported = errors.New("buildx dial-stdio not supported")

// builder is empty, so we need to check what the currently configured buildx builder is.
// This is stored int he buildx config in (typically) $HOME/.docker/buildx (the `dockercfg` lib determines where this actually is).
if b.builder == "" {
dt, err := os.ReadFile(filepath.Join(configBase, "current"))
if err != nil {
return pkgerrors.Wrap(err, "failed to read current builder")
func (b *BuildxEnv) dialStdio(ctx context.Context) error {
c, err := client.New(ctx, "", client.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
args := []string{"buildx", "dial-stdio", "--progress=plain"}
if b.builder != "" {
args = append(args, "--builder="+b.builder)
}

type ref struct {
Name string
Key string
}
var r ref
if err := json.Unmarshal(dt, &r); err != nil {
return err
cmd := exec.CommandContext(ctx, "docker", args...)
cmd.Env = os.Environ()

c1, c2 := net.Pipe()
cmd.Stdin = c1
cmd.Stdout = c1

// Use a pipe to check when the connection is actually complete
// Also write all of stderr to an error buffer so we can have more details
// in the error message when the command fails.
r, w := io.Pipe()
errBuf := bytes.NewBuffer(nil)
ww := io.MultiWriter(w, errBuf)
cmd.Stderr = ww

if err := cmd.Start(); err != nil {
return nil, err
}

if r.Name == "" {
// This is the "default" buildx instance, aka dockerd's built-in buildkit.
var tr transport.Doer
if r.Key != "" {
tr, err = transport.FromConnectionString(r.Key)
if err != nil {
return err
}
} else {
tr, err = transport.DefaultTransport()
if err != nil {
return err
}
go func() {
err := cmd.Wait()
c1.Close()
// pkgerrors.Wrap will return nil if err is nil, otherwise it will give
// us a wrapped error with the buffered stderr fromt he command.
w.CloseWithError(pkgerrors.Wrapf(err, "%s", errBuf))
}()

defer r.Close()

scanner := bufio.NewScanner(r)
for scanner.Scan() {
txt := strings.ToLower(scanner.Text())

if strings.HasPrefix(txt, "#1 dialing builder") && strings.HasSuffix(txt, "done") {
go func() {
// Continue draining stderr so the process does not get blocked
_, _ = io.Copy(io.Discard, r)
}()
break
}

b.client, err = client.New(ctx, "", buildkitopt.FromDocker(tr)...)
return err
}
if err := scanner.Err(); err != nil {
return nil, err
}

b.builder = r.Name
}
return c2, nil
}))

dt, err := os.ReadFile(filepath.Join(configBase, "instances", b.builder))
if err != nil {
return pkgerrors.Wrap(err, "failed to read buildx instance config")
}

var cfg buildxConfig
if err := json.Unmarshal(dt, &cfg); err != nil {
return pkgerrors.Wrap(err, "failed to unmarshal buildx config")
return err
}

if cfg.Driver != "docker-container" {
return pkgerrors.Errorf("unsupported buildx driver: %s", cfg.Driver)
}
b.client = c
return nil
}

if len(cfg.Nodes) == 0 {
return pkgerrors.Errorf("no buildx nodes configured")
// bootstrap is ultimately responsible for creating a buildkit client.
func (b *BuildxEnv) bootstrap(ctx context.Context) (retErr error) {
if b.client != nil {
return nil
}

// On a typical client this would be a single node, but there could be multiple registered with he same builder name.
// We'll just try them all until we find one that works.
var errs []error
for _, n := range cfg.Nodes {
tr, err := transport.FromConnectionString(n.Endpoint)
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", n.Endpoint, err))
continue
defer func() {
if retErr != nil {
return
}

dc := docker.NewClient(docker.WithTransport(tr))
ctr := dc.ContainerService().NewContainer(ctx, "buildx_buildkit_"+n.Name)
b.supportedOnce.Do(func() {
info, err := b.client.Info(ctx)
if err != nil {
b.supportedErr = pkgerrors.WithStack(err)
return
}

conn1, conn2 := net.Pipe()
ep, err := ctr.Exec(ctx, container.WithExecCmd("buildctl", "dial-stdio"), func(cfg *container.ExecConfig) {
cfg.Stdin = conn1
cfg.Stdout = conn1
cfg.Stderr = conn1
if !supportsFrontendAsInput(info) {
b.supportedErr = fmt.Errorf("buildkit version not supported: min version is v%s, got: %s", minVersion, info.BuildkitVersion.Version)
}
})
if err != nil {
conn1.Close()
conn2.Close()
errs = append(errs, fmt.Errorf("%s: %w", n.Endpoint, err))
continue
}

if err := ep.Start(ctx); err != nil {
conn1.Close()
conn2.Close()
errs = append(errs, fmt.Errorf("%s: %w", n.Endpoint, err))
continue
}

c, err := client.New(ctx, "", client.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return conn2, nil
}))
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", n.Endpoint, err))
continue
if b.supportedErr != nil {
b.client.Close()
b.client = nil
retErr = b.supportedErr
}
}()

b.client = c
b.ctr = ctr
b.docker = dc
return nil
ok, err := b.supportsDialStdio(ctx)
if err != nil {
return fmt.Errorf("%w: %w", errDialStdioNotSupported, err)
}

// Could not create a buildkit client, return all errors.
return errors.Join(errs...)
}

type buildxConfig struct {
Driver string
Nodes []struct {
Name string
Endpoint string
if !ok {
return errDialStdioNotSupported
}

return b.dialStdio(ctx)
}

func (b *BuildxEnv) Buildkit(ctx context.Context) (*client.Client, error) {
Expand Down Expand Up @@ -252,7 +238,7 @@ func withResolveLocal(so *client.SolveOpt) {
func (b *BuildxEnv) RunTest(ctx context.Context, t *testing.T, f gwclient.BuildFunc) {
c, err := b.Buildkit(ctx)
if err != nil {
t.Fatal(err)
t.Fatalf("%+v", err)
}

ch, done := displaySolveStatus(ctx, t)
Expand Down
Loading