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 }