From 1303715aba250eac60594a45e58161cd8bac6055 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Sat, 15 Apr 2023 15:11:32 +0900 Subject: [PATCH] Allow passing ResultContext from server to the client through grpcerror Signed-off-by: Kohei Tokunaga --- controller/build/build.go | 31 +++++++++++++ controller/errdefs/build.go | 34 ++++++++++++++ controller/errdefs/errdefs.pb.go | 77 ++++++++++++++++++++++++++++++++ controller/errdefs/errdefs.proto | 9 ++++ controller/errdefs/generate.go | 3 ++ controller/local/controller.go | 23 ++++++++-- controller/remote/client.go | 3 ++ controller/remote/controller.go | 6 ++- controller/remote/server.go | 28 +++++++++--- go.mod | 2 +- 10 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 controller/errdefs/build.go create mode 100644 controller/errdefs/errdefs.pb.go create mode 100644 controller/errdefs/errdefs.proto create mode 100644 controller/errdefs/generate.go diff --git a/controller/build/build.go b/controller/build/build.go index e5615b01..c9a3b24f 100644 --- a/controller/build/build.go +++ b/controller/build/build.go @@ -209,6 +209,9 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, ng *store.NodeGrou err = err1 } if err != nil { + if res != nil { + err = wrapResultContext(err, res) + } return nil, nil, err } @@ -379,3 +382,31 @@ func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.Ul } return dockeropts.NewUlimitOpt(&values) } + +// ResultContextError is an error type used for passing ResultContext from this package +// to the caller of RunBuild. This is only used when RunBuild fails with an error. +// When it succeeds without error, ResultContext is returned via non-error returned value. +// +// Caller can extract ResultContext from the error returned by RunBuild as the following: +// +// resp, res, buildErr := cbuild.RunBuild(ctx, req.Options, inR, statusChan) +// var re *cbuild.ResultContextError +// if errors.As(buildErr, &re) && re.ResultContext != nil { +// res = re.ResultContext +// } +type ResultContextError struct { + ResultContext *build.ResultContext + error +} + +// Unwrap returns the original error. +func (e *ResultContextError) Unwrap() error { + return e.error +} + +func wrapResultContext(wErr error, res *build.ResultContext) error { + if wErr == nil { + return nil + } + return &ResultContextError{ResultContext: res, error: wErr} +} diff --git a/controller/errdefs/build.go b/controller/errdefs/build.go new file mode 100644 index 00000000..47310b08 --- /dev/null +++ b/controller/errdefs/build.go @@ -0,0 +1,34 @@ +package errdefs + +import ( + "github.com/containerd/typeurl/v2" + "github.com/moby/buildkit/util/grpcerrors" +) + +func init() { + typeurl.Register((*Build)(nil), "github.com/docker/buildx", "errdefs.Build+json") +} + +type BuildError struct { + Build + error +} + +func (e *BuildError) Unwrap() error { + return e.error +} + +func (e *BuildError) ToProto() grpcerrors.TypedErrorProto { + return &e.Build +} + +func WrapBuild(err error, ref string) error { + if err == nil { + return nil + } + return &BuildError{Build: Build{Ref: ref}, error: err} +} + +func (b *Build) WrapError(err error) error { + return &BuildError{error: err, Build: *b} +} diff --git a/controller/errdefs/errdefs.pb.go b/controller/errdefs/errdefs.pb.go new file mode 100644 index 00000000..b7ea9a1e --- /dev/null +++ b/controller/errdefs/errdefs.pb.go @@ -0,0 +1,77 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: errdefs.proto + +package errdefs + +import ( + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + _ "github.com/moby/buildkit/solver/pb" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Build struct { + Ref string `protobuf:"bytes,1,opt,name=Ref,proto3" json:"Ref,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Build) Reset() { *m = Build{} } +func (m *Build) String() string { return proto.CompactTextString(m) } +func (*Build) ProtoMessage() {} +func (*Build) Descriptor() ([]byte, []int) { + return fileDescriptor_689dc58a5060aff5, []int{0} +} +func (m *Build) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Build.Unmarshal(m, b) +} +func (m *Build) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Build.Marshal(b, m, deterministic) +} +func (m *Build) XXX_Merge(src proto.Message) { + xxx_messageInfo_Build.Merge(m, src) +} +func (m *Build) XXX_Size() int { + return xxx_messageInfo_Build.Size(m) +} +func (m *Build) XXX_DiscardUnknown() { + xxx_messageInfo_Build.DiscardUnknown(m) +} + +var xxx_messageInfo_Build proto.InternalMessageInfo + +func (m *Build) GetRef() string { + if m != nil { + return m.Ref + } + return "" +} + +func init() { + proto.RegisterType((*Build)(nil), "errdefs.Build") +} + +func init() { proto.RegisterFile("errdefs.proto", fileDescriptor_689dc58a5060aff5) } + +var fileDescriptor_689dc58a5060aff5 = []byte{ + // 111 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x2d, 0x2a, 0x4a, + 0x49, 0x4d, 0x2b, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x87, 0x72, 0xa5, 0x74, 0xd2, + 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x73, 0xf3, 0x93, 0x2a, 0xf5, 0x93, + 0x4a, 0x33, 0x73, 0x52, 0xb2, 0x33, 0x4b, 0xf4, 0x8b, 0xf3, 0x73, 0xca, 0x52, 0x8b, 0xf4, 0x0b, + 0x92, 0xf4, 0xf3, 0x0b, 0xa0, 0xda, 0x94, 0x24, 0xb9, 0x58, 0x9d, 0x40, 0xf2, 0x42, 0x02, 0x5c, + 0xcc, 0x41, 0xa9, 0x69, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x20, 0x66, 0x12, 0x1b, 0x58, + 0x85, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x56, 0x52, 0x41, 0x91, 0x69, 0x00, 0x00, 0x00, +} diff --git a/controller/errdefs/errdefs.proto b/controller/errdefs/errdefs.proto new file mode 100644 index 00000000..9a7281fb --- /dev/null +++ b/controller/errdefs/errdefs.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package errdefs; + +import "github.com/moby/buildkit/solver/pb/ops.proto"; + +message Build { + string Ref = 1; +} \ No newline at end of file diff --git a/controller/errdefs/generate.go b/controller/errdefs/generate.go new file mode 100644 index 00000000..6f82237c --- /dev/null +++ b/controller/errdefs/generate.go @@ -0,0 +1,3 @@ +package errdefs + +//go:generate protoc -I=. -I=../../vendor/ --gogo_out=plugins=grpc:. errdefs.proto diff --git a/controller/local/controller.go b/controller/local/controller.go index 4c462d7d..b7eae6ad 100644 --- a/controller/local/controller.go +++ b/controller/local/controller.go @@ -9,6 +9,7 @@ import ( "github.com/docker/buildx/build" cbuild "github.com/docker/buildx/controller/build" "github.com/docker/buildx/controller/control" + controllererrors "github.com/docker/buildx/controller/errdefs" controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/controller/processes" "github.com/docker/buildx/util/ioset" @@ -40,11 +41,25 @@ func (b *localController) Build(ctx context.Context, options controllerapi.Build } defer b.buildOnGoing.Store(false) - resp, res, err := cbuild.RunBuild(ctx, b.dockerCli, options, in, progressMode, nil) - if err != nil { - return "", nil, err + resp, res, buildErr := cbuild.RunBuild(ctx, b.dockerCli, options, in, progressMode, nil) + if buildErr != nil { + var re *cbuild.ResultContextError + if errors.As(buildErr, &re) && re.ResultContext != nil { + res = re.ResultContext + } + } + if res != nil { + b.buildConfig = buildConfig{ + resultCtx: res, + buildOptions: &options, + } + if buildErr != nil { + buildErr = controllererrors.WrapBuild(buildErr, b.ref) + } + } + if buildErr != nil { + return "", nil, buildErr } - b.resultCtx = res return b.ref, resp, nil } diff --git a/controller/remote/client.go b/controller/remote/client.go index 9eeab3f1..f3ecfff4 100644 --- a/controller/remote/client.go +++ b/controller/remote/client.go @@ -13,6 +13,7 @@ import ( "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" "github.com/moby/buildkit/identity" + "github.com/moby/buildkit/util/grpcerrors" "github.com/pkg/errors" "golang.org/x/sync/errgroup" "google.golang.org/grpc" @@ -33,6 +34,8 @@ func NewClient(ctx context.Context, addr string) (*Client, error) { grpc.WithContextDialer(dialer.ContextDialer), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)), grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)), + grpc.WithUnaryInterceptor(grpcerrors.UnaryClientInterceptor), + grpc.WithStreamInterceptor(grpcerrors.StreamClientInterceptor), } conn, err := grpc.DialContext(ctx, dialer.DialAddress(addr), gopts...) if err != nil { diff --git a/controller/remote/controller.go b/controller/remote/controller.go index 81e69820..49de0d3c 100644 --- a/controller/remote/controller.go +++ b/controller/remote/controller.go @@ -24,6 +24,7 @@ import ( "github.com/docker/buildx/version" "github.com/docker/cli/cli/command" "github.com/moby/buildkit/client" + "github.com/moby/buildkit/util/grpcerrors" "github.com/pelletier/go-toml" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -161,7 +162,10 @@ func serveCmd(dockerCli command.Cli) *cobra.Command { if err != nil { return err } - rpc := grpc.NewServer() + rpc := grpc.NewServer( + grpc.UnaryInterceptor(grpcerrors.UnaryServerInterceptor), + grpc.StreamInterceptor(grpcerrors.StreamServerInterceptor), + ) controllerapi.RegisterControllerServer(rpc, b) doneCh := make(chan struct{}) errCh := make(chan error, 1) diff --git a/controller/remote/server.go b/controller/remote/server.go index ae387755..2b8a3406 100644 --- a/controller/remote/server.go +++ b/controller/remote/server.go @@ -8,6 +8,8 @@ import ( "time" "github.com/docker/buildx/build" + cbuild "github.com/docker/buildx/controller/build" + controllererrors "github.com/docker/buildx/controller/errdefs" "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/controller/processes" "github.com/docker/buildx/util/ioset" @@ -177,24 +179,40 @@ func (m *Server) Build(ctx context.Context, req *pb.BuildRequest) (*pb.BuildResp // Build the specified request ctx, cancel := context.WithCancel(ctx) defer cancel() - resp, res, err := m.buildFunc(ctx, req.Options, inR, statusChan) + resp, res, buildErr := m.buildFunc(ctx, req.Options, inR, statusChan) m.sessionMu.Lock() if s, ok := m.session[ref]; ok { - s.result = res - s.cancelBuild = cancel - m.session[ref] = s + if buildErr != nil { + var re *cbuild.ResultContextError + if errors.As(buildErr, &re) && re.ResultContext != nil { + res = re.ResultContext + } + } + if res != nil { + s.result = res + s.cancelBuild = cancel + s.buildOptions = req.Options + m.session[ref] = s + if buildErr != nil { + buildErr = controllererrors.WrapBuild(buildErr, ref) + } + } } else { m.sessionMu.Unlock() return nil, errors.Errorf("build: unknown key %v", ref) } m.sessionMu.Unlock() + if buildErr != nil { + return nil, buildErr + } + if resp == nil { resp = &client.SolveResponse{} } return &pb.BuildResponse{ ExporterResponse: resp.ExporterResponse, - }, err + }, nil } func (m *Server) Status(req *pb.StatusRequest, stream pb.Controller_StatusServer) error { diff --git a/go.mod b/go.mod index 2a15ef9b..75a326b0 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/compose-spec/compose-go v1.9.0 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.7.0 + github.com/containerd/typeurl/v2 v2.1.0 github.com/docker/cli v23.0.1+incompatible github.com/docker/cli-docs-tool v0.5.1 github.com/docker/distribution v2.8.1+incompatible @@ -82,7 +83,6 @@ require ( github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/containerd/ttrpc v1.2.1 // indirect - github.com/containerd/typeurl/v2 v2.1.0 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/distribution/v3 v3.0.0-20221103125252-ebfa2a0ac0a9 // indirect