Skip to content

Commit

Permalink
feat: Allow copa to use buildkit from dockerd
Browse files Browse the repository at this point in the history
This changes the default to attempt to connect to docker's built-in
buildkit instance.
If the built-in instance is not capable of supporting the features
required for copy it will log the issue and fallback to the default
buildkit buildkit address.

Also happens to check if the configured buildkit instance supports what
is required.

Signed-off-by: Brian Goff <[email protected]>
  • Loading branch information
cpuguy83 committed Aug 10, 2023
1 parent 45b0be1 commit 99bdb54
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 13 deletions.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/aquasecurity/trivy v0.44.1
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.7.3
github.com/cpuguy83/go-docker/buildkitopt v0.1.1
github.com/distribution/distribution v2.8.2+incompatible
github.com/docker/cli v24.0.5+incompatible
github.com/google/go-containerregistry v0.16.1
Expand All @@ -34,6 +35,7 @@ require (
github.com/containerd/continuity v0.4.1 // indirect
github.com/containerd/ttrpc v1.2.2 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/cpuguy83/go-docker v0.1.2
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
Expand Down Expand Up @@ -94,7 +96,7 @@ require (
golang.org/x/tools v0.10.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/grpc v1.55.0
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
6 changes: 5 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ 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/go-docker v0.1.2 h1:bHyfwV+yMw+IP2iL/0T5WU/g7Bpj+yhdlwoe6LEwA+A=
github.com/cpuguy83/go-docker v0.1.2/go.mod h1:tZURUlegjsgYPhkfkuPCeVrp1ocirH4P1yUexBpP25g=
github.com/cpuguy83/go-docker/buildkitopt v0.1.1 h1:zzg3taUURaoBzyowOqTtH1EoH3251oyceLS3202HF/M=
github.com/cpuguy83/go-docker/buildkitopt v0.1.1/go.mod h1:b4JdmqXsUZV8phNhTvShbMvFrwz80ZM3wVLAnXX41A0=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
Expand Down Expand Up @@ -725,7 +729,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
77 changes: 72 additions & 5 deletions pkg/buildkit/buildkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ package buildkit

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"

"github.com/containerd/console"
"github.com/containerd/containerd/remotes/docker"
"github.com/cpuguy83/go-docker/buildkitopt"
"github.com/cpuguy83/go-docker/transport"
"github.com/docker/cli/cli/config"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/imageutil"
"github.com/moby/buildkit/util/progress/progressui"
Expand All @@ -32,6 +39,10 @@ import (
"golang.org/x/sync/errgroup"
)

const (
DefaultAddr = "unix:///run/buildkit/buildkitd.sock"
)

type Config struct {
ImageName string
Client *client.Client
Expand Down Expand Up @@ -101,7 +112,66 @@ func resolveImageConfig(ctx context.Context, ref string, platform *ispec.Platfor
return dgst, config, nil
}

func InitializeBuildkitConfig(ctx context.Context, buildkitAddr, image string, manifest *types.UpdateManifest) (*Config, error) {
var errMissingCap = fmt.Errorf("missing required buildkit functionality")

// Buildkit llb ops required for copacetic
var requiredCaps = []apicaps.CapID{pb.CapMergeOp, pb.CapDiffOp}

// NewClient returns a new buildkit client with the given addr.
// If addr is empty it will first try to connect to docker's buildkit instance and then fallback to DefaultAddr.
func NewClient(ctx context.Context, addr string) (*client.Client, error) {
var bkOpts []client.ClientOpt
if addr == "" {
// First try using docker's buildkit instance if available
tr, err := transport.DefaultTransport()
if err != nil {
log.WithError(err).Info("Failed to initialize docker transport, falling back to default buildkit address")
addr = DefaultAddr
} else {
bkOpts = append(bkOpts, buildkitopt.FromDocker(tr)...)
}
}

c, err := client.New(ctx, addr, bkOpts...)
if err != nil {
return nil, err
}

_, err = c.Build(ctx, client.SolveOpt{}, "", func(ctx context.Context, client gateway.Client) (*gateway.Result, error) {
capset := client.BuildOpts().LLBCaps
var err error
for _, cap := range requiredCaps {
err = errors.Join(err, capset.Supports(cap))
}
if err != nil {
return nil, errors.Join(err, errMissingCap)
}
return &gateway.Result{}, nil
}, nil)
if err == nil {
return c, nil
}
c.Close()

if addr != "" {
return nil, err
}

if !errors.Is(err, errMissingCap) {
log.WithError(err).Warn("Failed to check buildkit LLB merge operation support, falling back to default buildkit address")
} else {
log.WithError(err).Warn("Docker integrated buildkit is too old, falling back to default buildkit address")
}

c, err = client.New(ctx, DefaultAddr)
if err != nil {
return nil, fmt.Errorf("failed to initialize buildkit client: %w", err)
}

return c, nil
}

func InitializeBuildkitConfig(ctx context.Context, client *client.Client, image string, manifest *types.UpdateManifest) (*Config, error) {
// Initialize buildkit config for the target image
config := Config{
ImageName: image,
Expand All @@ -128,10 +198,7 @@ func InitializeBuildkitConfig(ctx context.Context, buildkitAddr, image string, m
return nil, err
}

config.Client, err = client.New(ctx, buildkitAddr)
if err != nil {
return nil, err
}
config.Client = client

return &config, nil
}
Expand Down
181 changes: 181 additions & 0 deletions pkg/buildkit/buildkit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package buildkit

import (
"context"
"errors"
"net"
"path/filepath"
"testing"
"time"

controlapi "github.com/moby/buildkit/api/services/control"
types "github.com/moby/buildkit/api/types"
gateway "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/util/apicaps"
caps "github.com/moby/buildkit/util/apicaps/pb"
"google.golang.org/grpc"
)

type mockControlServer struct {
controlapi.ControlServer
}

func (s *mockControlServer) ListWorkers(context.Context, *controlapi.ListWorkersRequest) (*controlapi.ListWorkersResponse, error) {
return &controlapi.ListWorkersResponse{
Record: []*types.WorkerRecord{},
}, nil
}

func (s *mockControlServer) Session(controlapi.Control_SessionServer) error {
return nil
}

func (s *mockControlServer) Status(*controlapi.StatusRequest, controlapi.Control_StatusServer) error {
return nil
}

func (s *mockControlServer) Solve(context.Context, *controlapi.SolveRequest) (*controlapi.SolveResponse, error) {
return &controlapi.SolveResponse{}, nil
}

type mockLLBBridgeServer struct {
gateway.LLBBridgeServer
caps []caps.APICap
}

func (m *mockLLBBridgeServer) Ping(context.Context, *gateway.PingRequest) (*gateway.PongResponse, error) {
return &gateway.PongResponse{
FrontendAPICaps: m.caps,
LLBCaps: m.caps,
}, nil
}

func (m *mockLLBBridgeServer) Solve(context.Context, *gateway.SolveRequest) (*gateway.SolveResponse, error) {
return &gateway.SolveResponse{}, nil
}

func makeCapList(capIDs ...apicaps.CapID) []caps.APICap {
var (
ls apicaps.CapList
caps = make([]apicaps.Cap, 0, len(capIDs))
)

for _, id := range capIDs {
caps = append(caps, apicaps.Cap{
ID: id,
Enabled: true,
})
}

ls.Init(caps...)
return ls.All()
}

func newMockBuildkitAPI(t *testing.T, caps ...apicaps.CapID) string {
tmp := t.TempDir()
l, err := net.Listen("unix", filepath.Join(tmp, "buildkitd.sock"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { l.Close() })

srv := grpc.NewServer()
t.Cleanup(srv.Stop)

capList := makeCapList(caps...)
gateway.RegisterLLBBridgeServer(srv, &mockLLBBridgeServer{
LLBBridgeServer: &gateway.UnimplementedLLBBridgeServer{},
caps: capList,
})

go srv.Serve(l)

control := &mockControlServer{
ControlServer: &controlapi.UnimplementedControlServer{},
}
controlapi.RegisterControlServer(srv, control)

return l.Addr().String()
}

func unwrapErrors(err error) []error {
// `errors.Unwrap` uses this interface
// buildkit errors may be wrapped in this
type stdUnwrap interface {
Unwrap() error
}

// The type used by `errors.Join` uses this interface
type joinedUnwrap interface {
Unwrap() []error
}

var out []error
switch v := err.(type) {
case stdUnwrap:
return unwrapErrors(v.Unwrap())
case joinedUnwrap:
for _, e := range v.Unwrap() {
// multiple calls to `errors.Join` may result in nested wraps, so recurse on those errors
out = append(out, unwrapErrors(e)...)
}
default:
out = append(out, err)
}

return out
}

func checkMissingCapsError(t *testing.T, err error, caps ...apicaps.CapID) {
t.Helper()
lsErr := unwrapErrors(err)
found := make(map[apicaps.CapID]bool, len(caps))
for _, err := range lsErr {
check := &apicaps.CapError{}
if errors.As(err, &check) {
found[check.ID] = true
}
}
if len(found) != len(caps) {
t.Errorf("expected %d errors, got: %d", len(caps), len(found))
t.Error(lsErr)
}
}

func TestNewClient(t *testing.T) {
ctx := context.Background()

t.Run("custom buildkit addr", func(t *testing.T) {
t.Run("missing caps", func(t *testing.T) {
t.Parallel()
addr := newMockBuildkitAPI(t)
ctxT, cancel := context.WithTimeout(ctx, time.Second)
client, err := NewClient(ctxT, "unix://"+addr)
cancel()
defer func() {
if client != nil {
client.Close()
}
}()
if !errors.Is(err, errMissingCap) {
t.Fatalf("expected error %q, got: %s", errMissingCap, err)
}
checkMissingCapsError(t, err, requiredCaps...)
})
t.Run("with caps", func(t *testing.T) {
t.Parallel()
addr := newMockBuildkitAPI(t, requiredCaps...)

ctxT, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
client, err := NewClient(ctxT, "unix://"+addr)
if err != nil {
t.Fatal(err)
}
client.Close()
})
})

// TODO: Test with defaults, but then we need to control those socket paths.
// I considered doing this in a chroot, but it is fairly complicated to do so and requires root privileges anyway (or CAP_SYS_CHROOT).
}
7 changes: 2 additions & 5 deletions pkg/patch/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ import (
"context"
"time"

"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/spf13/cobra"
)

const (
defaultBuildkitAddr = "unix:///run/buildkit/buildkitd.sock"
)

type patchArgs struct {
appImage string
reportFile string
Expand Down Expand Up @@ -46,7 +43,7 @@ func NewPatchCmd() *cobra.Command {
flags.StringVarP(&ua.reportFile, "report", "r", "", "Vulnerability report file path")
flags.StringVarP(&ua.patchedTag, "tag", "t", "", "Tag for the patched image")
flags.StringVarP(&ua.workingFolder, "working-folder", "w", "", "Working folder, defaults to system temp folder")
flags.StringVarP(&ua.buildkitAddr, "addr", "a", defaultBuildkitAddr, "Address of buildkitd service, defaults to local buildkitd.sock")
flags.StringVarP(&ua.buildkitAddr, "addr", "a", "", "Address of buildkitd service, defaults to local docker daemon with fallback to "+buildkit.DefaultAddr)
flags.DurationVar(&ua.timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'")

if err := patchCmd.MarkFlagRequired("image"); err != nil {
Expand Down
8 changes: 7 additions & 1 deletion pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,14 @@ func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patc
}
log.Debugf("updates to apply: %v", updates)

client, err := buildkit.NewClient(ctx, buildkitAddr)
if err != nil {
return err
}
defer client.Close()

// Configure buildctl/client for use by package manager
config, err := buildkit.InitializeBuildkitConfig(ctx, buildkitAddr, image, updates)
config, err := buildkit.InitializeBuildkitConfig(ctx, client, image, updates)
if err != nil {
return err
}
Expand Down

0 comments on commit 99bdb54

Please sign in to comment.