github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/controller/build/build.go (about)

     1  package build
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/docker/buildx/build"
    12  	"github.com/docker/buildx/builder"
    13  	controllerapi "github.com/docker/buildx/controller/pb"
    14  	"github.com/docker/buildx/store"
    15  	"github.com/docker/buildx/store/storeutil"
    16  	"github.com/docker/buildx/util/buildflags"
    17  	"github.com/docker/buildx/util/confutil"
    18  	"github.com/docker/buildx/util/dockerutil"
    19  	"github.com/docker/buildx/util/platformutil"
    20  	"github.com/docker/buildx/util/progress"
    21  	"github.com/docker/cli/cli/command"
    22  	"github.com/docker/cli/cli/config"
    23  	dockeropts "github.com/docker/cli/opts"
    24  	"github.com/docker/go-units"
    25  	"github.com/moby/buildkit/client"
    26  	"github.com/moby/buildkit/session/auth/authprovider"
    27  	"github.com/moby/buildkit/util/grpcerrors"
    28  	"github.com/pkg/errors"
    29  	"google.golang.org/grpc/codes"
    30  )
    31  
    32  const defaultTargetName = "default"
    33  
    34  // RunBuild runs the specified build and returns the result.
    35  //
    36  // NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle,
    37  // this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can
    38  // inspect the result and debug the cause of that error.
    39  func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.BuildOptions, inStream io.Reader, progress progress.Writer, generateResult bool) (*client.SolveResponse, *build.ResultHandle, error) {
    40  	if in.NoCache && len(in.NoCacheFilter) > 0 {
    41  		return nil, nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together")
    42  	}
    43  
    44  	contexts := map[string]build.NamedContext{}
    45  	for name, path := range in.NamedContexts {
    46  		contexts[name] = build.NamedContext{Path: path}
    47  	}
    48  
    49  	opts := build.Options{
    50  		Inputs: build.Inputs{
    51  			ContextPath:    in.ContextPath,
    52  			DockerfilePath: in.DockerfileName,
    53  			InStream:       inStream,
    54  			NamedContexts:  contexts,
    55  		},
    56  		Ref:                    in.Ref,
    57  		BuildArgs:              in.BuildArgs,
    58  		CgroupParent:           in.CgroupParent,
    59  		ExtraHosts:             in.ExtraHosts,
    60  		Labels:                 in.Labels,
    61  		NetworkMode:            in.NetworkMode,
    62  		NoCache:                in.NoCache,
    63  		NoCacheFilter:          in.NoCacheFilter,
    64  		Pull:                   in.Pull,
    65  		ShmSize:                dockeropts.MemBytes(in.ShmSize),
    66  		Tags:                   in.Tags,
    67  		Target:                 in.Target,
    68  		Ulimits:                controllerUlimitOpt2DockerUlimit(in.Ulimits),
    69  		GroupRef:               in.GroupRef,
    70  		WithProvenanceResponse: in.WithProvenanceResponse,
    71  	}
    72  
    73  	platforms, err := platformutil.Parse(in.Platforms)
    74  	if err != nil {
    75  		return nil, nil, err
    76  	}
    77  	opts.Platforms = platforms
    78  
    79  	dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
    80  	opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(dockerConfig, nil))
    81  
    82  	secrets, err := controllerapi.CreateSecrets(in.Secrets)
    83  	if err != nil {
    84  		return nil, nil, err
    85  	}
    86  	opts.Session = append(opts.Session, secrets)
    87  
    88  	sshSpecs := in.SSH
    89  	if len(sshSpecs) == 0 && buildflags.IsGitSSH(in.ContextPath) {
    90  		sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
    91  	}
    92  	ssh, err := controllerapi.CreateSSH(sshSpecs)
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  	opts.Session = append(opts.Session, ssh)
    97  
    98  	outputs, err := controllerapi.CreateExports(in.Exports)
    99  	if err != nil {
   100  		return nil, nil, err
   101  	}
   102  	if in.ExportPush {
   103  		var pushUsed bool
   104  		for i := range outputs {
   105  			if outputs[i].Type == client.ExporterImage {
   106  				outputs[i].Attrs["push"] = "true"
   107  				pushUsed = true
   108  			}
   109  		}
   110  		if !pushUsed {
   111  			outputs = append(outputs, client.ExportEntry{
   112  				Type: client.ExporterImage,
   113  				Attrs: map[string]string{
   114  					"push": "true",
   115  				},
   116  			})
   117  		}
   118  	}
   119  	if in.ExportLoad {
   120  		var loadUsed bool
   121  		for i := range outputs {
   122  			if outputs[i].Type == client.ExporterDocker {
   123  				if _, ok := outputs[i].Attrs["dest"]; !ok {
   124  					loadUsed = true
   125  					break
   126  				}
   127  			}
   128  		}
   129  		if !loadUsed {
   130  			outputs = append(outputs, client.ExportEntry{
   131  				Type:  client.ExporterDocker,
   132  				Attrs: map[string]string{},
   133  			})
   134  		}
   135  	}
   136  
   137  	annotations, err := buildflags.ParseAnnotations(in.Annotations)
   138  	if err != nil {
   139  		return nil, nil, err
   140  	}
   141  	for _, o := range outputs {
   142  		for k, v := range annotations {
   143  			o.Attrs[k.String()] = v
   144  		}
   145  	}
   146  
   147  	opts.Exports = outputs
   148  
   149  	opts.CacheFrom = controllerapi.CreateCaches(in.CacheFrom)
   150  	opts.CacheTo = controllerapi.CreateCaches(in.CacheTo)
   151  
   152  	opts.Attests = controllerapi.CreateAttestations(in.Attests)
   153  
   154  	opts.SourcePolicy = in.SourcePolicy
   155  
   156  	allow, err := buildflags.ParseEntitlements(in.Allow)
   157  	if err != nil {
   158  		return nil, nil, err
   159  	}
   160  	opts.Allow = allow
   161  
   162  	if in.PrintFunc != nil {
   163  		opts.PrintFunc = &build.PrintFunc{
   164  			Name:         in.PrintFunc.Name,
   165  			Format:       in.PrintFunc.Format,
   166  			IgnoreStatus: in.PrintFunc.IgnoreStatus,
   167  		}
   168  	}
   169  
   170  	// key string used for kubernetes "sticky" mode
   171  	contextPathHash, err := filepath.Abs(in.ContextPath)
   172  	if err != nil {
   173  		contextPathHash = in.ContextPath
   174  	}
   175  
   176  	// TODO: this should not be loaded this side of the controller api
   177  	b, err := builder.New(dockerCli,
   178  		builder.WithName(in.Builder),
   179  		builder.WithContextPathHash(contextPathHash),
   180  	)
   181  	if err != nil {
   182  		return nil, nil, err
   183  	}
   184  	if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil {
   185  		return nil, nil, errors.Wrapf(err, "failed to update builder last activity time")
   186  	}
   187  	nodes, err := b.LoadNodes(ctx)
   188  	if err != nil {
   189  		return nil, nil, err
   190  	}
   191  
   192  	resp, res, err := buildTargets(ctx, dockerCli, nodes, map[string]build.Options{defaultTargetName: opts}, progress, generateResult)
   193  	err = wrapBuildError(err, false)
   194  	if err != nil {
   195  		// NOTE: buildTargets can return *build.ResultHandle even on error.
   196  		return nil, res, err
   197  	}
   198  	return resp, res, nil
   199  }
   200  
   201  // buildTargets runs the specified build and returns the result.
   202  //
   203  // NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle,
   204  // this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can
   205  // inspect the result and debug the cause of that error.
   206  func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.Node, opts map[string]build.Options, progress progress.Writer, generateResult bool) (*client.SolveResponse, *build.ResultHandle, error) {
   207  	var res *build.ResultHandle
   208  	var resp map[string]*client.SolveResponse
   209  	var err error
   210  	if generateResult {
   211  		var mu sync.Mutex
   212  		var idx int
   213  		resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) {
   214  			mu.Lock()
   215  			defer mu.Unlock()
   216  			if res == nil || driverIndex < idx {
   217  				idx, res = driverIndex, gotRes
   218  			}
   219  		})
   220  	} else {
   221  		resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress)
   222  	}
   223  	if err != nil {
   224  		return nil, res, err
   225  	}
   226  	return resp[defaultTargetName], res, err
   227  }
   228  
   229  func wrapBuildError(err error, bake bool) error {
   230  	if err == nil {
   231  		return nil
   232  	}
   233  	st, ok := grpcerrors.AsGRPCStatus(err)
   234  	if ok {
   235  		if st.Code() == codes.Unimplemented && strings.Contains(st.Message(), "unsupported frontend capability moby.buildkit.frontend.contexts") {
   236  			msg := "current frontend does not support --build-context."
   237  			if bake {
   238  				msg = "current frontend does not support defining additional contexts for targets."
   239  			}
   240  			msg += " Named contexts are supported since Dockerfile v1.4. Use #syntax directive in Dockerfile or update to latest BuildKit."
   241  			return &wrapped{err, msg}
   242  		}
   243  	}
   244  	return err
   245  }
   246  
   247  type wrapped struct {
   248  	err error
   249  	msg string
   250  }
   251  
   252  func (w *wrapped) Error() string {
   253  	return w.msg
   254  }
   255  
   256  func (w *wrapped) Unwrap() error {
   257  	return w.err
   258  }
   259  
   260  func updateLastActivity(dockerCli command.Cli, ng *store.NodeGroup) error {
   261  	txn, release, err := storeutil.GetStore(dockerCli)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	defer release()
   266  	return txn.UpdateLastActivity(ng)
   267  }
   268  
   269  func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.UlimitOpt {
   270  	if u == nil {
   271  		return nil
   272  	}
   273  	values := make(map[string]*units.Ulimit)
   274  	for k, v := range u.Values {
   275  		values[k] = &units.Ulimit{
   276  			Name: v.Name,
   277  			Hard: v.Hard,
   278  			Soft: v.Soft,
   279  		}
   280  	}
   281  	return dockeropts.NewUlimitOpt(&values)
   282  }