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

     1  package builder
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"encoding/json"
     7  	"net/url"
     8  	"os"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/docker/buildx/driver"
    15  	k8sutil "github.com/docker/buildx/driver/kubernetes/util"
    16  	remoteutil "github.com/docker/buildx/driver/remote/util"
    17  	"github.com/docker/buildx/localstate"
    18  	"github.com/docker/buildx/store"
    19  	"github.com/docker/buildx/store/storeutil"
    20  	"github.com/docker/buildx/util/confutil"
    21  	"github.com/docker/buildx/util/dockerutil"
    22  	"github.com/docker/buildx/util/imagetools"
    23  	"github.com/docker/buildx/util/progress"
    24  	"github.com/docker/cli/cli/command"
    25  	dopts "github.com/docker/cli/opts"
    26  	"github.com/google/shlex"
    27  	"github.com/moby/buildkit/util/progress/progressui"
    28  	"github.com/pkg/errors"
    29  	"github.com/spf13/pflag"
    30  	"golang.org/x/sync/errgroup"
    31  )
    32  
    33  // Builder represents an active builder object
    34  type Builder struct {
    35  	*store.NodeGroup
    36  	driverFactory driverFactory
    37  	nodes         []Node
    38  	opts          builderOpts
    39  	err           error
    40  }
    41  
    42  type builderOpts struct {
    43  	dockerCli       command.Cli
    44  	name            string
    45  	txn             *store.Txn
    46  	contextPathHash string
    47  	validate        bool
    48  }
    49  
    50  // Option provides a variadic option for configuring the builder.
    51  type Option func(b *Builder)
    52  
    53  // WithName sets builder name.
    54  func WithName(name string) Option {
    55  	return func(b *Builder) {
    56  		b.opts.name = name
    57  	}
    58  }
    59  
    60  // WithStore sets a store instance used at init.
    61  func WithStore(txn *store.Txn) Option {
    62  	return func(b *Builder) {
    63  		b.opts.txn = txn
    64  	}
    65  }
    66  
    67  // WithContextPathHash is used for determining pods in k8s driver instance.
    68  func WithContextPathHash(contextPathHash string) Option {
    69  	return func(b *Builder) {
    70  		b.opts.contextPathHash = contextPathHash
    71  	}
    72  }
    73  
    74  // WithSkippedValidation skips builder context validation.
    75  func WithSkippedValidation() Option {
    76  	return func(b *Builder) {
    77  		b.opts.validate = false
    78  	}
    79  }
    80  
    81  // New initializes a new builder client
    82  func New(dockerCli command.Cli, opts ...Option) (_ *Builder, err error) {
    83  	b := &Builder{
    84  		opts: builderOpts{
    85  			dockerCli: dockerCli,
    86  			validate:  true,
    87  		},
    88  	}
    89  	for _, opt := range opts {
    90  		opt(b)
    91  	}
    92  
    93  	if b.opts.txn == nil {
    94  		// if store instance is nil we create a short-lived one using the
    95  		// default store and ensure we release it on completion
    96  		var release func()
    97  		b.opts.txn, release, err = storeutil.GetStore(dockerCli)
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  		defer release()
   102  	}
   103  
   104  	if b.opts.name != "" {
   105  		if b.NodeGroup, err = storeutil.GetNodeGroup(b.opts.txn, dockerCli, b.opts.name); err != nil {
   106  			return nil, err
   107  		}
   108  	} else {
   109  		if b.NodeGroup, err = storeutil.GetCurrentInstance(b.opts.txn, dockerCli); err != nil {
   110  			return nil, err
   111  		}
   112  	}
   113  	if b.opts.validate {
   114  		if err = b.Validate(); err != nil {
   115  			return nil, err
   116  		}
   117  	}
   118  
   119  	return b, nil
   120  }
   121  
   122  // Validate validates builder context
   123  func (b *Builder) Validate() error {
   124  	if b.NodeGroup != nil && b.NodeGroup.DockerContext {
   125  		list, err := b.opts.dockerCli.ContextStore().List()
   126  		if err != nil {
   127  			return err
   128  		}
   129  		currentContext := b.opts.dockerCli.CurrentContext()
   130  		for _, l := range list {
   131  			if l.Name == b.Name && l.Name != currentContext {
   132  				return errors.Errorf("use `docker --context=%s buildx` to switch to context %q", l.Name, l.Name)
   133  			}
   134  		}
   135  	}
   136  	return nil
   137  }
   138  
   139  // ContextName returns builder context name if available.
   140  func (b *Builder) ContextName() string {
   141  	ctxbuilders, err := b.opts.dockerCli.ContextStore().List()
   142  	if err != nil {
   143  		return ""
   144  	}
   145  	for _, cb := range ctxbuilders {
   146  		if b.NodeGroup.Driver == "docker" && len(b.NodeGroup.Nodes) == 1 && b.NodeGroup.Nodes[0].Endpoint == cb.Name {
   147  			return cb.Name
   148  		}
   149  	}
   150  	return ""
   151  }
   152  
   153  // ImageOpt returns registry auth configuration
   154  func (b *Builder) ImageOpt() (imagetools.Opt, error) {
   155  	return storeutil.GetImageConfig(b.opts.dockerCli, b.NodeGroup)
   156  }
   157  
   158  // Boot bootstrap a builder
   159  func (b *Builder) Boot(ctx context.Context) (bool, error) {
   160  	toBoot := make([]int, 0, len(b.nodes))
   161  	for idx, d := range b.nodes {
   162  		if d.Err != nil || d.Driver == nil || d.DriverInfo == nil {
   163  			continue
   164  		}
   165  		if d.DriverInfo.Status != driver.Running {
   166  			toBoot = append(toBoot, idx)
   167  		}
   168  	}
   169  	if len(toBoot) == 0 {
   170  		return false, nil
   171  	}
   172  
   173  	printer, err := progress.NewPrinter(context.TODO(), os.Stderr, progressui.AutoMode)
   174  	if err != nil {
   175  		return false, err
   176  	}
   177  
   178  	baseCtx := ctx
   179  	eg, _ := errgroup.WithContext(ctx)
   180  	errCh := make(chan error, len(toBoot))
   181  	for _, idx := range toBoot {
   182  		func(idx int) {
   183  			eg.Go(func() error {
   184  				pw := progress.WithPrefix(printer, b.NodeGroup.Nodes[idx].Name, len(toBoot) > 1)
   185  				_, err := driver.Boot(ctx, baseCtx, b.nodes[idx].Driver, pw)
   186  				if err != nil {
   187  					b.nodes[idx].Err = err
   188  					errCh <- err
   189  				}
   190  				return nil
   191  			})
   192  		}(idx)
   193  	}
   194  
   195  	err = eg.Wait()
   196  	close(errCh)
   197  	err1 := printer.Wait()
   198  	if err == nil {
   199  		err = err1
   200  	}
   201  
   202  	if err == nil && len(errCh) == len(toBoot) {
   203  		return false, <-errCh
   204  	}
   205  	return true, err
   206  }
   207  
   208  // Inactive checks if all nodes are inactive for this builder.
   209  func (b *Builder) Inactive() bool {
   210  	for _, d := range b.nodes {
   211  		if d.DriverInfo != nil && d.DriverInfo.Status == driver.Running {
   212  			return false
   213  		}
   214  	}
   215  	return true
   216  }
   217  
   218  // Err returns error if any.
   219  func (b *Builder) Err() error {
   220  	return b.err
   221  }
   222  
   223  type driverFactory struct {
   224  	driver.Factory
   225  	once sync.Once
   226  }
   227  
   228  // Factory returns the driver factory.
   229  func (b *Builder) Factory(ctx context.Context, dialMeta map[string][]string) (_ driver.Factory, err error) {
   230  	b.driverFactory.once.Do(func() {
   231  		if b.Driver != "" {
   232  			b.driverFactory.Factory, err = driver.GetFactory(b.Driver, true)
   233  			if err != nil {
   234  				return
   235  			}
   236  		} else {
   237  			// empty driver means nodegroup was implicitly created as a default
   238  			// driver for a docker context and allows falling back to a
   239  			// docker-container driver for older daemon that doesn't support
   240  			// buildkit (< 18.06).
   241  			ep := b.NodeGroup.Nodes[0].Endpoint
   242  			var dockerapi *dockerutil.ClientAPI
   243  			dockerapi, err = dockerutil.NewClientAPI(b.opts.dockerCli, b.NodeGroup.Nodes[0].Endpoint)
   244  			if err != nil {
   245  				return
   246  			}
   247  			// check if endpoint is healthy is needed to determine the driver type.
   248  			// if this fails then can't continue with driver selection.
   249  			if _, err = dockerapi.Ping(ctx); err != nil {
   250  				return
   251  			}
   252  			b.driverFactory.Factory, err = driver.GetDefaultFactory(ctx, ep, dockerapi, false, dialMeta)
   253  			if err != nil {
   254  				return
   255  			}
   256  			b.Driver = b.driverFactory.Factory.Name()
   257  		}
   258  	})
   259  	return b.driverFactory.Factory, err
   260  }
   261  
   262  func (b *Builder) MarshalJSON() ([]byte, error) {
   263  	var berr string
   264  	if b.err != nil {
   265  		berr = strings.TrimSpace(b.err.Error())
   266  	}
   267  	return json.Marshal(struct {
   268  		Name         string
   269  		Driver       string
   270  		LastActivity time.Time `json:",omitempty"`
   271  		Dynamic      bool
   272  		Nodes        []Node
   273  		Err          string `json:",omitempty"`
   274  	}{
   275  		Name:         b.Name,
   276  		Driver:       b.Driver,
   277  		LastActivity: b.LastActivity,
   278  		Dynamic:      b.Dynamic,
   279  		Nodes:        b.nodes,
   280  		Err:          berr,
   281  	})
   282  }
   283  
   284  // GetBuilders returns all builders
   285  func GetBuilders(dockerCli command.Cli, txn *store.Txn) ([]*Builder, error) {
   286  	storeng, err := txn.List()
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	builders := make([]*Builder, len(storeng))
   292  	seen := make(map[string]struct{})
   293  	for i, ng := range storeng {
   294  		b, err := New(dockerCli,
   295  			WithName(ng.Name),
   296  			WithStore(txn),
   297  			WithSkippedValidation(),
   298  		)
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		builders[i] = b
   303  		seen[b.NodeGroup.Name] = struct{}{}
   304  	}
   305  
   306  	contexts, err := dockerCli.ContextStore().List()
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	sort.Slice(contexts, func(i, j int) bool {
   311  		return contexts[i].Name < contexts[j].Name
   312  	})
   313  
   314  	for _, c := range contexts {
   315  		// if a context has the same name as an instance from the store, do not
   316  		// add it to the builders list. An instance from the store takes
   317  		// precedence over context builders.
   318  		if _, ok := seen[c.Name]; ok {
   319  			continue
   320  		}
   321  		b, err := New(dockerCli,
   322  			WithName(c.Name),
   323  			WithStore(txn),
   324  			WithSkippedValidation(),
   325  		)
   326  		if err != nil {
   327  			return nil, err
   328  		}
   329  		builders = append(builders, b)
   330  	}
   331  
   332  	return builders, nil
   333  }
   334  
   335  type CreateOpts struct {
   336  	Name                string
   337  	Driver              string
   338  	NodeName            string
   339  	Platforms           []string
   340  	BuildkitdFlags      string
   341  	BuildkitdConfigFile string
   342  	DriverOpts          []string
   343  	Use                 bool
   344  	Endpoint            string
   345  	Append              bool
   346  }
   347  
   348  func Create(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts CreateOpts) (*Builder, error) {
   349  	var err error
   350  
   351  	if opts.Name == "default" {
   352  		return nil, errors.Errorf("default is a reserved name and cannot be used to identify builder instance")
   353  	} else if opts.Append && opts.Name == "" {
   354  		return nil, errors.Errorf("append requires a builder name")
   355  	}
   356  
   357  	name := opts.Name
   358  	if name == "" {
   359  		name, err = store.GenerateName(txn)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  	}
   364  
   365  	if !opts.Append {
   366  		contexts, err := dockerCli.ContextStore().List()
   367  		if err != nil {
   368  			return nil, err
   369  		}
   370  		for _, c := range contexts {
   371  			if c.Name == name {
   372  				return nil, errors.Errorf("instance name %q already exists as context builder", name)
   373  			}
   374  		}
   375  	}
   376  
   377  	ng, err := txn.NodeGroupByName(name)
   378  	if err != nil {
   379  		if os.IsNotExist(errors.Cause(err)) {
   380  			if opts.Append && opts.Name != "" {
   381  				return nil, errors.Errorf("failed to find instance %q for append", opts.Name)
   382  			}
   383  		} else {
   384  			return nil, err
   385  		}
   386  	}
   387  
   388  	buildkitHost := os.Getenv("BUILDKIT_HOST")
   389  
   390  	driverName := opts.Driver
   391  	if driverName == "" {
   392  		if ng != nil {
   393  			driverName = ng.Driver
   394  		} else if opts.Endpoint == "" && buildkitHost != "" {
   395  			driverName = "remote"
   396  		} else {
   397  			f, err := driver.GetDefaultFactory(ctx, opts.Endpoint, dockerCli.Client(), true, nil)
   398  			if err != nil {
   399  				return nil, err
   400  			}
   401  			if f == nil {
   402  				return nil, errors.Errorf("no valid drivers found")
   403  			}
   404  			driverName = f.Name()
   405  		}
   406  	}
   407  
   408  	if ng != nil {
   409  		if opts.NodeName == "" && !opts.Append {
   410  			return nil, errors.Errorf("existing instance for %q but no append mode, specify the node name to make changes for existing instances", name)
   411  		}
   412  		if driverName != ng.Driver {
   413  			return nil, errors.Errorf("existing instance for %q but has mismatched driver %q", name, ng.Driver)
   414  		}
   415  	}
   416  
   417  	if _, err := driver.GetFactory(driverName, true); err != nil {
   418  		return nil, err
   419  	}
   420  
   421  	ngOriginal := ng
   422  	if ngOriginal != nil {
   423  		ngOriginal = ngOriginal.Copy()
   424  	}
   425  
   426  	if ng == nil {
   427  		ng = &store.NodeGroup{
   428  			Name:   name,
   429  			Driver: driverName,
   430  		}
   431  	}
   432  
   433  	driverOpts, err := csvToMap(opts.DriverOpts)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  
   438  	buildkitdFlags, err := parseBuildkitdFlags(opts.BuildkitdFlags, driverName, driverOpts)
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	var ep string
   444  	var setEp bool
   445  	switch {
   446  	case driverName == "kubernetes":
   447  		if opts.Endpoint != "" {
   448  			return nil, errors.Errorf("kubernetes driver does not support endpoint args %q", opts.Endpoint)
   449  		}
   450  		// generate node name if not provided to avoid duplicated endpoint
   451  		// error: https://github.com/docker/setup-buildx-action/issues/215
   452  		nodeName := opts.NodeName
   453  		if nodeName == "" {
   454  			nodeName, err = k8sutil.GenerateNodeName(name, txn)
   455  			if err != nil {
   456  				return nil, err
   457  			}
   458  		}
   459  		// naming endpoint to make append works
   460  		ep = (&url.URL{
   461  			Scheme: driverName,
   462  			Path:   "/" + name,
   463  			RawQuery: (&url.Values{
   464  				"deployment": {nodeName},
   465  				"kubeconfig": {os.Getenv("KUBECONFIG")},
   466  			}).Encode(),
   467  		}).String()
   468  		setEp = false
   469  	case driverName == "remote":
   470  		if opts.Endpoint != "" {
   471  			ep = opts.Endpoint
   472  		} else if buildkitHost != "" {
   473  			ep = buildkitHost
   474  		} else {
   475  			return nil, errors.Errorf("no remote endpoint provided")
   476  		}
   477  		ep, err = validateBuildkitEndpoint(ep)
   478  		if err != nil {
   479  			return nil, err
   480  		}
   481  		setEp = true
   482  	case opts.Endpoint != "":
   483  		ep, err = validateEndpoint(dockerCli, opts.Endpoint)
   484  		if err != nil {
   485  			return nil, err
   486  		}
   487  		setEp = true
   488  	default:
   489  		if dockerCli.CurrentContext() == "default" && dockerCli.DockerEndpoint().TLSData != nil {
   490  			return nil, errors.Errorf("could not create a builder instance with TLS data loaded from environment. Please use `docker context create <context-name>` to create a context for current environment and then create a builder instance with context set to <context-name>")
   491  		}
   492  		ep, err = dockerutil.GetCurrentEndpoint(dockerCli)
   493  		if err != nil {
   494  			return nil, err
   495  		}
   496  		setEp = false
   497  	}
   498  
   499  	buildkitdConfigFile := opts.BuildkitdConfigFile
   500  	if buildkitdConfigFile == "" {
   501  		// if buildkit daemon config is not provided, check if the default one
   502  		// is available and use it
   503  		if f, ok := confutil.DefaultConfigFile(dockerCli); ok {
   504  			buildkitdConfigFile = f
   505  		}
   506  	}
   507  
   508  	if err := ng.Update(opts.NodeName, ep, opts.Platforms, setEp, opts.Append, buildkitdFlags, buildkitdConfigFile, driverOpts); err != nil {
   509  		return nil, err
   510  	}
   511  
   512  	if err := txn.Save(ng); err != nil {
   513  		return nil, err
   514  	}
   515  
   516  	b, err := New(dockerCli,
   517  		WithName(ng.Name),
   518  		WithStore(txn),
   519  		WithSkippedValidation(),
   520  	)
   521  	if err != nil {
   522  		return nil, err
   523  	}
   524  
   525  	timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
   526  	defer cancel()
   527  
   528  	nodes, err := b.LoadNodes(timeoutCtx, WithData())
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  
   533  	for _, node := range nodes {
   534  		if err := node.Err; err != nil {
   535  			err := errors.Errorf("failed to initialize builder %s (%s): %s", ng.Name, node.Name, err)
   536  			var err2 error
   537  			if ngOriginal == nil {
   538  				err2 = txn.Remove(ng.Name)
   539  			} else {
   540  				err2 = txn.Save(ngOriginal)
   541  			}
   542  			if err2 != nil {
   543  				return nil, errors.Errorf("could not rollback to previous state: %s", err2)
   544  			}
   545  			return nil, err
   546  		}
   547  	}
   548  
   549  	if opts.Use && ep != "" {
   550  		current, err := dockerutil.GetCurrentEndpoint(dockerCli)
   551  		if err != nil {
   552  			return nil, err
   553  		}
   554  		if err := txn.SetCurrent(current, ng.Name, false, false); err != nil {
   555  			return nil, err
   556  		}
   557  	}
   558  
   559  	return b, nil
   560  }
   561  
   562  type LeaveOpts struct {
   563  	Name     string
   564  	NodeName string
   565  }
   566  
   567  func Leave(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts LeaveOpts) error {
   568  	if opts.Name == "" {
   569  		return errors.Errorf("leave requires instance name")
   570  	}
   571  	if opts.NodeName == "" {
   572  		return errors.Errorf("leave requires node name")
   573  	}
   574  
   575  	ng, err := txn.NodeGroupByName(opts.Name)
   576  	if err != nil {
   577  		if os.IsNotExist(errors.Cause(err)) {
   578  			return errors.Errorf("failed to find instance %q for leave", opts.Name)
   579  		}
   580  		return err
   581  	}
   582  
   583  	if err := ng.Leave(opts.NodeName); err != nil {
   584  		return err
   585  	}
   586  
   587  	ls, err := localstate.New(confutil.ConfigDir(dockerCli))
   588  	if err != nil {
   589  		return err
   590  	}
   591  	if err := ls.RemoveBuilderNode(ng.Name, opts.NodeName); err != nil {
   592  		return err
   593  	}
   594  
   595  	return txn.Save(ng)
   596  }
   597  
   598  func csvToMap(in []string) (map[string]string, error) {
   599  	if len(in) == 0 {
   600  		return nil, nil
   601  	}
   602  	m := make(map[string]string, len(in))
   603  	for _, s := range in {
   604  		csvReader := csv.NewReader(strings.NewReader(s))
   605  		fields, err := csvReader.Read()
   606  		if err != nil {
   607  			return nil, err
   608  		}
   609  		for _, v := range fields {
   610  			p := strings.SplitN(v, "=", 2)
   611  			if len(p) != 2 {
   612  				return nil, errors.Errorf("invalid value %q, expecting k=v", v)
   613  			}
   614  			m[p[0]] = p[1]
   615  		}
   616  	}
   617  	return m, nil
   618  }
   619  
   620  // validateEndpoint validates that endpoint is either a context or a docker host
   621  func validateEndpoint(dockerCli command.Cli, ep string) (string, error) {
   622  	dem, err := dockerutil.GetDockerEndpoint(dockerCli, ep)
   623  	if err == nil && dem != nil {
   624  		if ep == "default" {
   625  			return dem.Host, nil
   626  		}
   627  		return ep, nil
   628  	}
   629  	h, err := dopts.ParseHost(true, ep)
   630  	if err != nil {
   631  		return "", errors.Wrapf(err, "failed to parse endpoint %s", ep)
   632  	}
   633  	return h, nil
   634  }
   635  
   636  // validateBuildkitEndpoint validates that endpoint is a valid buildkit host
   637  func validateBuildkitEndpoint(ep string) (string, error) {
   638  	if err := remoteutil.IsValidEndpoint(ep); err != nil {
   639  		return "", err
   640  	}
   641  	return ep, nil
   642  }
   643  
   644  // parseBuildkitdFlags parses buildkit flags
   645  func parseBuildkitdFlags(inp string, driver string, driverOpts map[string]string) (res []string, err error) {
   646  	if inp != "" {
   647  		res, err = shlex.Split(inp)
   648  		if err != nil {
   649  			return nil, errors.Wrap(err, "failed to parse buildkit flags")
   650  		}
   651  	}
   652  
   653  	var allowInsecureEntitlements []string
   654  	flags := pflag.NewFlagSet("buildkitd", pflag.ContinueOnError)
   655  	flags.Usage = func() {}
   656  	flags.StringArrayVar(&allowInsecureEntitlements, "allow-insecure-entitlement", nil, "")
   657  	_ = flags.Parse(res)
   658  
   659  	var hasNetworkHostEntitlement bool
   660  	for _, e := range allowInsecureEntitlements {
   661  		if e == "network.host" {
   662  			hasNetworkHostEntitlement = true
   663  			break
   664  		}
   665  	}
   666  
   667  	if v, ok := driverOpts["network"]; ok && v == "host" && !hasNetworkHostEntitlement && driver == "docker-container" {
   668  		// always set network.host entitlement if user has set network=host
   669  		res = append(res, "--allow-insecure-entitlement=network.host")
   670  	} else if len(allowInsecureEntitlements) == 0 && (driver == "kubernetes" || driver == "docker-container") {
   671  		// set network.host entitlement if user does not provide any as
   672  		// network is isolated for container drivers.
   673  		res = append(res, "--allow-insecure-entitlement=network.host")
   674  	}
   675  
   676  	return res, nil
   677  }