diff --git a/bake/remote.go b/bake/remote.go index 23cb180a621..26365f024a6 100644 --- a/bake/remote.go +++ b/bake/remote.go @@ -6,8 +6,8 @@ import ( "context" "strings" - "github.com/docker/buildx/build" "github.com/docker/buildx/driver" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" "github.com/moby/buildkit/client/llb" @@ -20,7 +20,7 @@ type Input struct { URL string } -func ReadRemoteFiles(ctx context.Context, dis []build.DriverInfo, url string, names []string, pw progress.Writer) ([]File, *Input, error) { +func ReadRemoteFiles(ctx context.Context, drivers []builderutil.Driver, url string, names []string, pw progress.Writer) ([]File, *Input, error) { var filename string st, ok := detectGitContext(url) if !ok { @@ -33,8 +33,8 @@ func ReadRemoteFiles(ctx context.Context, dis []build.DriverInfo, url string, na inp := &Input{State: st, URL: url} var files []File - var di *build.DriverInfo - for _, d := range dis { + var di *builderutil.Driver + for _, d := range drivers { if d.Err == nil { di = &d continue diff --git a/build/build.go b/build/build.go index 1783e087a51..4ea07f22cc1 100644 --- a/build/build.go +++ b/build/build.go @@ -19,13 +19,14 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" "github.com/docker/buildx/driver" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/imagetools" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/resolver" "github.com/docker/cli/opts" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - dockerclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/urlutil" "github.com/moby/buildkit/client" @@ -81,20 +82,8 @@ type Inputs struct { DockerfileInline string } -type DriverInfo struct { - Driver driver.Driver - Name string - Platform []specs.Platform - Err error - ImageOpt imagetools.Opt -} - -type DockerAPI interface { - DockerAPI(name string) (dockerclient.APIClient, error) -} - -func filterAvailableDrivers(drivers []DriverInfo) ([]DriverInfo, error) { - out := make([]DriverInfo, 0, len(drivers)) +func filterAvailableDrivers(drivers []builderutil.Driver) ([]builderutil.Driver, error) { + out := make([]builderutil.Driver, 0, len(drivers)) err := errors.Errorf("no drivers found") for _, di := range drivers { if di.Err == nil && di.Driver != nil { @@ -140,7 +129,7 @@ func allIndexes(l int) []int { return out } -func ensureBooted(ctx context.Context, drivers []DriverInfo, idxs []int, pw progress.Writer) ([]*client.Client, error) { +func ensureBooted(ctx context.Context, drivers []builderutil.Driver, idxs []int, pw progress.Writer) ([]*client.Client, error) { clients := make([]*client.Client, len(drivers)) baseCtx := ctx @@ -186,7 +175,7 @@ func splitToDriverPairs(availablePlatforms map[string]int, opt map[string]Option return m } -func resolveDrivers(ctx context.Context, drivers []DriverInfo, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { +func resolveDrivers(ctx context.Context, drivers []builderutil.Driver, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { dps, clients, err := resolveDriversBase(ctx, drivers, opt, pw) if err != nil { return nil, nil, err @@ -227,10 +216,10 @@ func resolveDrivers(ctx context.Context, drivers []DriverInfo, opt map[string]Op return dps, clients, nil } -func resolveDriversBase(ctx context.Context, drivers []DriverInfo, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { +func resolveDriversBase(ctx context.Context, drivers []builderutil.Driver, opt map[string]Options, pw progress.Writer) (map[string][]driverPair, []*client.Client, error) { availablePlatforms := map[string]int{} for i, d := range drivers { - for _, p := range d.Platform { + for _, p := range d.Platforms { availablePlatforms[platforms.Format(p)] = i } } @@ -580,7 +569,7 @@ func toSolveOpt(ctx context.Context, d driver.Driver, multiDriver bool, opt Opti return &so, releaseF, nil } -func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { +func Build(ctx context.Context, drivers []builderutil.Driver, opt map[string]Options, docker *storeutil.DockerClient, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { if len(drivers) == 0 { return nil, errors.Errorf("driver required for build") } @@ -633,7 +622,7 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do } opt.Platforms = dp.platforms so, release, err := toSolveOpt(ctx, d, multiDriver, opt, dp.bopts, configDir, w, func(name string) (io.WriteCloser, func(), error) { - return newDockerLoader(ctx, docker, name, w) + return docker.LoadImage(ctx, name, w) }) if err != nil { return nil, err @@ -1094,40 +1083,6 @@ func notSupported(d driver.Driver, f driver.Feature) error { type dockerLoadCallback func(name string) (io.WriteCloser, func(), error) -func newDockerLoader(ctx context.Context, d DockerAPI, name string, status progress.Writer) (io.WriteCloser, func(), error) { - c, err := d.DockerAPI(name) - if err != nil { - return nil, nil, err - } - - pr, pw := io.Pipe() - done := make(chan struct{}) - - ctx, cancel := context.WithCancel(ctx) - var w *waitingWriter - w = &waitingWriter{ - PipeWriter: pw, - f: func() { - resp, err := c.ImageLoad(ctx, pr, false) - defer close(done) - if err != nil { - pr.CloseWithError(err) - w.mu.Lock() - w.err = err - w.mu.Unlock() - return - } - prog := progress.WithPrefix(status, "", false) - progress.FromReader(prog, "importing to docker", resp.Body) - }, - done: done, - cancel: cancel, - } - return w, func() { - pr.Close() - }, nil -} - func noDefaultLoad() bool { v, ok := os.LookupEnv("BUILDX_NO_DEFAULT_LOAD") if !ok { @@ -1140,34 +1095,6 @@ func noDefaultLoad() bool { return b } -type waitingWriter struct { - *io.PipeWriter - f func() - once sync.Once - mu sync.Mutex - err error - done chan struct{} - cancel func() -} - -func (w *waitingWriter) Write(dt []byte) (int, error) { - w.once.Do(func() { - go w.f() - }) - return w.PipeWriter.Write(dt) -} - -func (w *waitingWriter) Close() error { - err := w.PipeWriter.Close() - <-w.done - if err == nil { - w.mu.Lock() - defer w.mu.Unlock() - return w.err - } - return err -} - // handle https://github.com/moby/moby/pull/10858 func handleLowercaseDockerfile(dir, p string) string { if filepath.Base(p) != "Dockerfile" { diff --git a/commands/bake.go b/commands/bake.go index c6bc9ba8b3a..c9c7bd14804 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -9,6 +9,8 @@ import ( "github.com/containerd/containerd/platforms" "github.com/docker/buildx/bake" "github.com/docker/buildx/build" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/tracing" @@ -87,16 +89,28 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) (err error } }() - dis, err := getInstanceOrDefault(ctx, dockerCli, in.builder, contextPathHash) + txn, release, err := storeutil.GetStore(dockerCli) if err != nil { return err } + defer release() + + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err + } + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, contextPathHash); err != nil { + return err + } var files []bake.File var inp *bake.Input if url != "" { - files, inp, err = bake.ReadRemoteFiles(ctx, dis, url, in.files, printer) + files, inp, err = bake.ReadRemoteFiles(ctx, builder.Drivers, url, in.files, printer) } else { files, err = bake.ReadLocalFiles(in.files) } @@ -146,7 +160,7 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) (err error return nil } - resp, err := build.Build(ctx, dis, bo, dockerAPI(dockerCli), confutil.ConfigDir(dockerCli), printer) + resp, err := build.Build(ctx, builder.Drivers, bo, storeutil.NewDockerClient(dockerCli), confutil.ConfigDir(dockerCli), printer) if err != nil { return err } diff --git a/commands/build.go b/commands/build.go index 4bce23fc201..baa918d1165 100644 --- a/commands/build.go +++ b/commands/build.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/docker/buildx/build" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/platformutil" @@ -204,7 +206,24 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { contextPathHash = in.contextPath } - imageID, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile) + txn, release, err := storeutil.GetStore(dockerCli) + if err != nil { + return err + } + defer release() + + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err + } + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, contextPathHash); err != nil { + return err + } + + imageID, err := buildTargets(ctx, dockerCli, builder.Drivers, map[string]build.Options{defaultTargetName: opts}, in.progress, in.metadataFile) if err != nil { return err } @@ -215,18 +234,13 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { return nil } -func buildTargets(ctx context.Context, dockerCli command.Cli, opts map[string]build.Options, progressMode, contextPathHash, instance string, metadataFile string) (imageID string, err error) { - dis, err := getInstanceOrDefault(ctx, dockerCli, instance, contextPathHash) - if err != nil { - return "", err - } - +func buildTargets(ctx context.Context, dockerCli command.Cli, drivers []builderutil.Driver, opts map[string]build.Options, progressMode string, metadataFile string) (imageID string, err error) { ctx2, cancel := context.WithCancel(context.TODO()) defer cancel() printer := progress.NewPrinter(ctx2, os.Stderr, progressMode) - resp, err := build.Build(ctx, dis, opts, dockerAPI(dockerCli), confutil.ConfigDir(dockerCli), printer) + resp, err := build.Build(ctx, drivers, opts, storeutil.NewDockerClient(dockerCli), confutil.ConfigDir(dockerCli), printer) err1 := printer.Wait() if err == nil { err = err1 diff --git a/commands/create.go b/commands/create.go index 1d4169fb167..a06cefbce20 100644 --- a/commands/create.go +++ b/commands/create.go @@ -13,9 +13,11 @@ import ( "github.com/docker/buildx/driver" "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/cobrautil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/opts" "github.com/google/shlex" "github.com/moby/buildkit/util/appcontext" "github.com/pkg/errors" @@ -143,7 +145,6 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error { if dockerCli.CurrentContext() == "default" && dockerCli.DockerEndpoint().TLSData != nil { return errors.Errorf("could not create a builder instance with TLS data loaded from environment. Please use `docker context create ` to create a context for current environment and then create a builder instance with `docker buildx create `") } - ep, err = storeutil.GetCurrentEndpoint(dockerCli) if err != nil { return err @@ -185,17 +186,17 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error { } } - ngi := &nginfo{ng: ng} - - timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) - defer cancel() - - if err = loadNodeGroupData(timeoutCtx, dockerCli, ngi); err != nil { - return err - } - if in.bootstrap { - if _, err = boot(ctx, ngi); err != nil { + builder, err := builderutil.New(dockerCli, txn, ng.Name) + if err != nil { + return err + } + timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + if err = builder.LoadDrivers(timeoutCtx, true, ""); err != nil { + return err + } + if _, err = builder.Boot(ctx); err != nil { return err } } @@ -263,3 +264,18 @@ func csvToMap(in []string) (map[string]string, error) { } return m, nil } + +func validateEndpoint(dockerCli command.Cli, ep string) (string, error) { + dem, err := storeutil.GetDockerEndpoint(dockerCli, ep) + if err == nil && dem != nil { + if ep == "default" { + return dem.Host, nil + } + return ep, nil + } + h, err := opts.ParseHost(true, ep) + if err != nil { + return "", errors.Wrapf(err, "failed to parse endpoint %s", ep) + } + return h, nil +} diff --git a/commands/diskusage.go b/commands/diskusage.go index 223cd93bec1..32d6018cb5d 100644 --- a/commands/diskusage.go +++ b/commands/diskusage.go @@ -6,7 +6,8 @@ import ( "os" "text/tabwriter" - "github.com/docker/buildx/build" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" @@ -23,30 +24,43 @@ type duOptions struct { verbose bool } -func runDiskUsage(dockerCli command.Cli, opts duOptions) error { +func runDiskUsage(dockerCli command.Cli, in duOptions) error { ctx := appcontext.Context() - pi, err := toBuildkitPruneInfo(opts.filter.Value()) + pi, err := toBuildkitPruneInfo(in.filter.Value()) if err != nil { return err } - dis, err := getInstanceOrDefault(ctx, dockerCli, opts.builder, "") + txn, release, err := storeutil.GetStore(dockerCli) if err != nil { return err } + defer release() - for _, di := range dis { + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err + } + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, ""); err != nil { + return err + } + + drivers := builder.Drivers + for _, di := range drivers { if di.Err != nil { return err } } - out := make([][]*client.UsageInfo, len(dis)) + out := make([][]*client.UsageInfo, len(drivers)) eg, ctx := errgroup.WithContext(ctx) - for i, di := range dis { - func(i int, di build.DriverInfo) { + for i, di := range drivers { + func(i int, di builderutil.Driver) { eg.Go(func() error { if di.Driver != nil { c, err := di.Driver.Client(ctx) @@ -75,7 +89,7 @@ func runDiskUsage(dockerCli command.Cli, opts duOptions) error { if du == nil { continue } - if opts.verbose { + if in.verbose { printVerbose(tw, du) } else { if first { @@ -90,7 +104,7 @@ func runDiskUsage(dockerCli command.Cli, opts duOptions) error { } } - if opts.filter.Value().Len() == 0 { + if in.filter.Value().Len() == 0 { printSummary(tw, out) } diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go index c42d4715f41..f1528b8eb32 100644 --- a/commands/imagetools/create.go +++ b/commands/imagetools/create.go @@ -6,8 +6,8 @@ import ( "io/ioutil" "strings" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/imagetools" "github.com/docker/cli/cli/command" "github.com/docker/distribution/reference" @@ -110,21 +110,14 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error { } defer release() - var ng *store.NodeGroup - - if in.builder != "" { - ng, err = storeutil.GetNodeGroup(txn, dockerCli, in.builder) - if err != nil { - return err - } - } else { - ng, err = storeutil.GetCurrentInstance(txn, dockerCli) - if err != nil { - return err - } + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err } - - imageopt, err := storeutil.GetImageConfig(dockerCli, ng) + if err = builder.Validate(); err != nil { + return err + } + imageopt, err := builder.GetImageOpt() if err != nil { return err } diff --git a/commands/imagetools/inspect.go b/commands/imagetools/inspect.go index 7a6892f34aa..c79e71c8e2d 100644 --- a/commands/imagetools/inspect.go +++ b/commands/imagetools/inspect.go @@ -5,8 +5,8 @@ import ( "os" "github.com/containerd/containerd/images" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/imagetools" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -29,24 +29,18 @@ func runInspect(dockerCli command.Cli, in inspectOptions, name string) error { } defer release() - var ng *store.NodeGroup - - if in.builder != "" { - ng, err = storeutil.GetNodeGroup(txn, dockerCli, in.builder) - if err != nil { - return err - } - } else { - ng, err = storeutil.GetCurrentInstance(txn, dockerCli) - if err != nil { - return err - } + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err } - - imageopt, err := storeutil.GetImageConfig(dockerCli, ng) + if err = builder.Validate(); err != nil { + return err + } + imageopt, err := builder.GetImageOpt() if err != nil { return err } + r := imagetools.New(imageopt) dt, desc, err := r.Get(ctx, name) diff --git a/commands/inspect.go b/commands/inspect.go index ecb1c61626e..bedfbf2a7e9 100644 --- a/commands/inspect.go +++ b/commands/inspect.go @@ -8,8 +8,8 @@ import ( "text/tabwriter" "time" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/platformutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -31,81 +31,57 @@ func runInspect(dockerCli command.Cli, in inspectOptions) error { } defer release() - var ng *store.NodeGroup - - if in.builder != "" { - ng, err = storeutil.GetNodeGroup(txn, dockerCli, in.builder) - if err != nil { - return err - } - } else { - ng, err = storeutil.GetCurrentInstance(txn, dockerCli) - if err != nil { - return err - } - } - - if ng == nil { - ng = &store.NodeGroup{ - Name: "default", - Nodes: []store.Node{{ - Name: "default", - Endpoint: "default", - }}, - } + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err } - ngi := &nginfo{ng: ng} - timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() - err = loadNodeGroupData(timeoutCtx, dockerCli, ngi) + err = builder.LoadDrivers(timeoutCtx, true, "") - var bootNgi *nginfo + var bootBuilder *builderutil.Builder if in.bootstrap { var ok bool - ok, err = boot(ctx, ngi) + ok, err = builder.Boot(ctx) if err != nil { return err } - bootNgi = ngi + bootBuilder = builder if ok { - ngi = &nginfo{ng: ng} - err = loadNodeGroupData(ctx, dockerCli, ngi) + err = builder.LoadDrivers(timeoutCtx, true, "") } } w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - fmt.Fprintf(w, "Name:\t%s\n", ngi.ng.Name) - fmt.Fprintf(w, "Driver:\t%s\n", ngi.ng.Driver) + fmt.Fprintf(w, "Name:\t%s\n", builder.NodeGroup.Name) + fmt.Fprintf(w, "Driver:\t%s\n", builder.NodeGroup.Driver) if err != nil { fmt.Fprintf(w, "Error:\t%s\n", err.Error()) - } else if ngi.err != nil { - fmt.Fprintf(w, "Error:\t%s\n", ngi.err.Error()) + } else if builder.Err != nil { + fmt.Fprintf(w, "Error:\t%s\n", builder.Err.Error()) } if err == nil { fmt.Fprintln(w, "") fmt.Fprintln(w, "Nodes:") - for i, n := range ngi.ng.Nodes { + for i, n := range builder.NodeGroup.Nodes { if i != 0 { fmt.Fprintln(w, "") } fmt.Fprintf(w, "Name:\t%s\n", n.Name) fmt.Fprintf(w, "Endpoint:\t%s\n", n.Endpoint) - if err := ngi.drivers[i].di.Err; err != nil { - fmt.Fprintf(w, "Error:\t%s\n", err.Error()) - } else if err := ngi.drivers[i].err; err != nil { + if err := builder.Drivers[i].Err; err != nil { fmt.Fprintf(w, "Error:\t%s\n", err.Error()) - } else if bootNgi != nil && len(bootNgi.drivers) > i && bootNgi.drivers[i].err != nil { - fmt.Fprintf(w, "Error:\t%s\n", bootNgi.drivers[i].err.Error()) + } else if bootBuilder != nil && len(bootBuilder.Drivers) > i && bootBuilder.Drivers[i].Err != nil { + fmt.Fprintf(w, "Error:\t%s\n", bootBuilder.Drivers[i].Err.Error()) } else { - fmt.Fprintf(w, "Status:\t%s\n", ngi.drivers[i].info.Status) + fmt.Fprintf(w, "Status:\t%s\n", builder.Drivers[i].Info.Status) if len(n.Flags) > 0 { fmt.Fprintf(w, "Flags:\t%s\n", strings.Join(n.Flags, " ")) } - fmt.Fprintf(w, "Platforms:\t%s\n", strings.Join(platformutil.FormatInGroups(n.Platforms, ngi.drivers[i].platforms), ", ")) + fmt.Fprintf(w, "Platforms:\t%s\n", strings.Join(platformutil.FormatInGroups(n.Platforms, builder.Drivers[i].Platforms), ", ")) } } } diff --git a/commands/ls.go b/commands/ls.go index 25c87782295..fe0fb16b5ca 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -9,8 +9,8 @@ import ( "text/tabwriter" "time" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/buildx/util/cobrautil" "github.com/docker/buildx/util/platformutil" "github.com/docker/cli/cli" @@ -32,74 +32,36 @@ func runLs(dockerCli command.Cli, in lsOptions) error { } defer release() - ctx, cancel := context.WithTimeout(ctx, 20*time.Second) - defer cancel() - - ll, err := txn.List() + builders, err := builderutil.GetBuilders(dockerCli, txn) if err != nil { return err } - builders := make([]*nginfo, len(ll)) - for i, ng := range ll { - builders[i] = &nginfo{ng: ng} - } - - list, err := dockerCli.ContextStore().List() - if err != nil { - return err - } - ctxbuilders := make([]*nginfo, len(list)) - for i, l := range list { - ctxbuilders[i] = &nginfo{ng: &store.NodeGroup{ - Name: l.Name, - Nodes: []store.Node{{ - Name: l.Name, - Endpoint: l.Name, - }}, - }} - } - - builders = append(builders, ctxbuilders...) - - eg, _ := errgroup.WithContext(ctx) + timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + eg, _ := errgroup.WithContext(timeoutCtx) for _, b := range builders { - func(b *nginfo) { + func(b *builderutil.Builder) { eg.Go(func() error { - err = loadNodeGroupData(ctx, dockerCli, b) - if b.err == nil && err != nil { - b.err = err + err = b.LoadDrivers(timeoutCtx, true, "") + if b.Err == nil && err != nil { + b.Err = err } return nil }) }(b) } - if err := eg.Wait(); err != nil { return err } - currentName := "default" - current, err := storeutil.GetCurrentInstance(txn, dockerCli) - if err != nil { - return err - } - if current != nil { - currentName = current.Name - if current.Name == "default" { - currentName = current.Nodes[0].Endpoint - } - } - w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) fmt.Fprintf(w, "NAME/NODE\tDRIVER/ENDPOINT\tSTATUS\tPLATFORMS\n") - currentSet := false for _, b := range builders { - if !currentSet && b.ng.Name == currentName { - b.ng.Name += " *" - currentSet = true + if b.NodeGroup.Current { + b.NodeGroup.Name += " *" } printngi(w, b) } @@ -109,29 +71,27 @@ func runLs(dockerCli command.Cli, in lsOptions) error { return nil } -func printngi(w io.Writer, ngi *nginfo) { +func printngi(w io.Writer, b *builderutil.Builder) { var err string - if ngi.err != nil { - err = ngi.err.Error() + if b.Err != nil { + err = b.Err.Error() } - fmt.Fprintf(w, "%s\t%s\t%s\t\n", ngi.ng.Name, ngi.ng.Driver, err) - if ngi.err == nil { - for idx, n := range ngi.ng.Nodes { - d := ngi.drivers[idx] + fmt.Fprintf(w, "%s\t%s\t%s\t\n", b.NodeGroup.Name, b.NodeGroup.Driver, err) + if b.Err == nil { + for idx, n := range b.NodeGroup.Nodes { + d := b.Drivers[idx] var err string - if d.err != nil { - err = d.err.Error() - } else if d.di.Err != nil { - err = d.di.Err.Error() + if d.Err != nil { + err = d.Err.Error() } var status string - if d.info != nil { - status = d.info.Status.String() + if d.Info != nil { + status = d.Info.Status.String() } if err != "" { fmt.Fprintf(w, " %s\t%s\t%s\n", n.Name, n.Endpoint, err) } else { - fmt.Fprintf(w, " %s\t%s\t%s\t%s\n", n.Name, n.Endpoint, status, strings.Join(platformutil.FormatInGroups(n.Platforms, d.platforms), ", ")) + fmt.Fprintf(w, " %s\t%s\t%s\t%s\n", n.Name, n.Endpoint, status, strings.Join(platformutil.FormatInGroups(n.Platforms, d.Platforms), ", ")) } } } diff --git a/commands/prune.go b/commands/prune.go index b4207ae5aa2..287d2087f4c 100644 --- a/commands/prune.go +++ b/commands/prune.go @@ -7,7 +7,8 @@ import ( "text/tabwriter" "time" - "github.com/docker/buildx/build" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" @@ -34,10 +35,10 @@ const ( allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?` ) -func runPrune(dockerCli command.Cli, opts pruneOptions) error { +func runPrune(dockerCli command.Cli, in pruneOptions) error { ctx := appcontext.Context() - pruneFilters := opts.filter.Value() + pruneFilters := in.filter.Value() pruneFilters = command.PruneFilters(dockerCli, pruneFilters) pi, err := toBuildkitPruneInfo(pruneFilters) @@ -46,20 +47,33 @@ func runPrune(dockerCli command.Cli, opts pruneOptions) error { } warning := normalWarning - if opts.all { + if in.all { warning = allCacheWarning } - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + if !in.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return nil } - dis, err := getInstanceOrDefault(ctx, dockerCli, opts.builder, "") + txn, release, err := storeutil.GetStore(dockerCli) if err != nil { return err } + defer release() - for _, di := range dis { + builder, err := builderutil.New(dockerCli, txn, in.builder) + if err != nil { + return err + } + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, ""); err != nil { + return err + } + + drivers := builder.Drivers + for _, di := range drivers { if di.Err != nil { return err } @@ -76,7 +90,7 @@ func runPrune(dockerCli command.Cli, opts pruneOptions) error { defer close(printed) for du := range ch { total += du.Size - if opts.verbose { + if in.verbose { printVerbose(tw, []*client.UsageInfo{&du}) } else { if first { @@ -90,8 +104,8 @@ func runPrune(dockerCli command.Cli, opts pruneOptions) error { }() eg, ctx := errgroup.WithContext(ctx) - for _, di := range dis { - func(di build.DriverInfo) { + for _, di := range drivers { + func(di builderutil.Driver) { eg.Go(func() error { if di.Driver != nil { c, err := di.Driver.Client(ctx) @@ -99,10 +113,10 @@ func runPrune(dockerCli command.Cli, opts pruneOptions) error { return err } popts := []client.PruneOption{ - client.WithKeepOpt(pi.KeepDuration, opts.keepStorage.Value()), + client.WithKeepOpt(pi.KeepDuration, in.keepStorage.Value()), client.WithFilter(pi.Filter), } - if opts.all { + if in.all { popts = append(popts, client.PruneAll) } return c.Prune(ctx, ch, popts...) diff --git a/commands/rm.go b/commands/rm.go index ab19bbef9a5..aac21b0a0f4 100644 --- a/commands/rm.go +++ b/commands/rm.go @@ -3,8 +3,8 @@ package commands import ( "context" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/moby/buildkit/util/appcontext" @@ -26,31 +26,22 @@ func runRm(dockerCli command.Cli, in rmOptions) error { } defer release() - if in.builder != "" { - ng, err := storeutil.GetNodeGroup(txn, dockerCli, in.builder) - if err != nil { - return err - } - err1 := rm(ctx, dockerCli, ng, in.keepState, in.keepDaemon) - if err := txn.Remove(ng.Name); err != nil { - return err - } - return err1 - } - - ng, err := storeutil.GetCurrentInstance(txn, dockerCli) + builder, err := builderutil.New(dockerCli, txn, in.builder) if err != nil { return err } - if ng != nil { - err1 := rm(ctx, dockerCli, ng, in.keepState, in.keepDaemon) - if err := txn.Remove(ng.Name); err != nil { - return err - } - return err1 + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, ""); err != nil { + return err } - return nil + err1 := rm(ctx, builder.Drivers, in.keepState, in.keepDaemon) + if err := txn.Remove(builder.NodeGroup.Name); err != nil { + return err + } + return err1 } func rmCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { @@ -76,22 +67,18 @@ func rmCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { return cmd } -func rm(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup, keepState, keepDaemon bool) error { - dis, err := driversForNodeGroup(ctx, dockerCli, ng, "") - if err != nil { - return err - } - for _, di := range dis { +func rm(ctx context.Context, drivers []builderutil.Driver, keepState, keepDaemon bool) (err error) { + for _, di := range drivers { if di.Driver == nil { continue } // Do not stop the buildkitd daemon when --keep-daemon is provided if !keepDaemon { - if err := di.Driver.Stop(ctx, true); err != nil { + if err = di.Driver.Stop(ctx, true); err != nil { return err } } - if err := di.Driver.Rm(ctx, true, !keepState, !keepDaemon); err != nil { + if err = di.Driver.Rm(ctx, true, !keepState, !keepDaemon); err != nil { return err } if di.Err != nil { diff --git a/commands/stop.go b/commands/stop.go index a5fe5a45f55..6ffa23b2fca 100644 --- a/commands/stop.go +++ b/commands/stop.go @@ -3,8 +3,8 @@ package commands import ( "context" - "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/builderutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/moby/buildkit/util/appcontext" @@ -24,26 +24,18 @@ func runStop(dockerCli command.Cli, in stopOptions) error { } defer release() - if in.builder != "" { - ng, err := storeutil.GetNodeGroup(txn, dockerCli, in.builder) - if err != nil { - return err - } - if err := stop(ctx, dockerCli, ng); err != nil { - return err - } - return nil - } - - ng, err := storeutil.GetCurrentInstance(txn, dockerCli) + builder, err := builderutil.New(dockerCli, txn, in.builder) if err != nil { return err } - if ng != nil { - return stop(ctx, dockerCli, ng) + if err = builder.Validate(); err != nil { + return err + } + if err = builder.LoadDrivers(ctx, false, ""); err != nil { + return err } - return stopCurrent(ctx, dockerCli) + return stop(ctx, builder.Drivers) } func stopCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { @@ -65,30 +57,8 @@ func stopCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { return cmd } -func stop(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup) error { - dis, err := driversForNodeGroup(ctx, dockerCli, ng, "") - if err != nil { - return err - } - for _, di := range dis { - if di.Driver != nil { - if err := di.Driver.Stop(ctx, true); err != nil { - return err - } - } - if di.Err != nil { - err = di.Err - } - } - return err -} - -func stopCurrent(ctx context.Context, dockerCli command.Cli) error { - dis, err := getDefaultDrivers(ctx, dockerCli, false, "") - if err != nil { - return err - } - for _, di := range dis { +func stop(ctx context.Context, drivers []builderutil.Driver) (err error) { + for _, di := range drivers { if di.Driver != nil { if err := di.Driver.Stop(ctx, true); err != nil { return err diff --git a/commands/util.go b/commands/util.go deleted file mode 100644 index d4ee40adf0b..00000000000 --- a/commands/util.go +++ /dev/null @@ -1,430 +0,0 @@ -package commands - -import ( - "context" - "net/url" - "os" - "strings" - - "github.com/docker/buildx/build" - "github.com/docker/buildx/driver" - "github.com/docker/buildx/store" - "github.com/docker/buildx/store/storeutil" - "github.com/docker/buildx/util/platformutil" - "github.com/docker/buildx/util/progress" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/context/docker" - "github.com/docker/cli/cli/context/kubernetes" - ctxstore "github.com/docker/cli/cli/context/store" - dopts "github.com/docker/cli/opts" - dockerclient "github.com/docker/docker/client" - specs "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - "k8s.io/client-go/tools/clientcmd" -) - -// validateEndpoint validates that endpoint is either a context or a docker host -func validateEndpoint(dockerCli command.Cli, ep string) (string, error) { - de, err := storeutil.GetDockerEndpoint(dockerCli, ep) - if err == nil && de != "" { - if ep == "default" { - return de, nil - } - return ep, nil - } - h, err := dopts.ParseHost(true, ep) - if err != nil { - return "", errors.Wrapf(err, "failed to parse endpoint %s", ep) - } - return h, nil -} - -// driversForNodeGroup returns drivers for a nodegroup instance -func driversForNodeGroup(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup, contextPathHash string) ([]build.DriverInfo, error) { - eg, _ := errgroup.WithContext(ctx) - - dis := make([]build.DriverInfo, len(ng.Nodes)) - - var f driver.Factory - if ng.Driver != "" { - f = driver.GetFactory(ng.Driver, true) - if f == nil { - return nil, errors.Errorf("failed to find driver %q", f) - } - } else { - dockerapi, err := clientForEndpoint(dockerCli, ng.Nodes[0].Endpoint) - if err != nil { - return nil, err - } - f, err = driver.GetDefaultFactory(ctx, dockerapi, false) - if err != nil { - return nil, err - } - ng.Driver = f.Name() - } - imageopt, err := storeutil.GetImageConfig(dockerCli, ng) - if err != nil { - return nil, err - } - - for i, n := range ng.Nodes { - func(i int, n store.Node) { - eg.Go(func() error { - di := build.DriverInfo{ - Name: n.Name, - Platform: n.Platforms, - } - defer func() { - dis[i] = di - }() - dockerapi, err := clientForEndpoint(dockerCli, n.Endpoint) - if err != nil { - di.Err = err - return nil - } - // TODO: replace the following line with dockerclient.WithAPIVersionNegotiation option in clientForEndpoint - dockerapi.NegotiateAPIVersion(ctx) - - contextStore := dockerCli.ContextStore() - - var kcc driver.KubeClientConfig - kcc, err = configFromContext(n.Endpoint, contextStore) - if err != nil { - // err is returned if n.Endpoint is non-context name like "unix:///var/run/docker.sock". - // try again with name="default". - // FIXME: n should retain real context name. - kcc, err = configFromContext("default", contextStore) - if err != nil { - logrus.Error(err) - } - } - - tryToUseKubeConfigInCluster := false - if kcc == nil { - tryToUseKubeConfigInCluster = true - } else { - if _, err := kcc.ClientConfig(); err != nil { - tryToUseKubeConfigInCluster = true - } - } - if tryToUseKubeConfigInCluster { - kccInCluster := driver.KubeClientConfigInCluster{} - if _, err := kccInCluster.ClientConfig(); err == nil { - logrus.Debug("using kube config in cluster") - kcc = kccInCluster - } - } - - d, err := driver.GetDriver(ctx, "buildx_buildkit_"+n.Name, f, dockerapi, imageopt.Auth, kcc, n.Flags, n.Files, n.DriverOpts, n.Platforms, contextPathHash) - if err != nil { - di.Err = err - return nil - } - di.Driver = d - di.ImageOpt = imageopt - return nil - }) - }(i, n) - } - - if err := eg.Wait(); err != nil { - return nil, err - } - - return dis, nil -} - -func configFromContext(endpointName string, s ctxstore.Reader) (clientcmd.ClientConfig, error) { - if strings.HasPrefix(endpointName, "kubernetes://") { - u, _ := url.Parse(endpointName) - if kubeconfig := u.Query().Get("kubeconfig"); kubeconfig != "" { - _ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfig) - } - rules := clientcmd.NewDefaultClientConfigLoadingRules() - apiConfig, err := rules.Load() - if err != nil { - return nil, err - } - return clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}), nil - } - return kubernetes.ConfigFromContext(endpointName, s) -} - -// clientForEndpoint returns a docker client for an endpoint -func clientForEndpoint(dockerCli command.Cli, name string) (dockerclient.APIClient, error) { - list, err := dockerCli.ContextStore().List() - if err != nil { - return nil, err - } - for _, l := range list { - if l.Name == name { - dep, ok := l.Endpoints["docker"] - if !ok { - return nil, errors.Errorf("context %q does not have a Docker endpoint", name) - } - epm, ok := dep.(docker.EndpointMeta) - if !ok { - return nil, errors.Errorf("endpoint %q is not of type EndpointMeta, %T", dep, dep) - } - ep, err := docker.WithTLSData(dockerCli.ContextStore(), name, epm) - if err != nil { - return nil, err - } - clientOpts, err := ep.ClientOpts() - if err != nil { - return nil, err - } - return dockerclient.NewClientWithOpts(clientOpts...) - } - } - - ep := docker.Endpoint{ - EndpointMeta: docker.EndpointMeta{ - Host: name, - }, - } - - clientOpts, err := ep.ClientOpts() - if err != nil { - return nil, err - } - - return dockerclient.NewClientWithOpts(clientOpts...) -} - -func getInstanceOrDefault(ctx context.Context, dockerCli command.Cli, instance, contextPathHash string) ([]build.DriverInfo, error) { - var defaultOnly bool - - if instance == "default" && instance != dockerCli.CurrentContext() { - return nil, errors.Errorf("use `docker --context=default buildx` to switch to default context") - } - if instance == "default" || instance == dockerCli.CurrentContext() { - instance = "" - defaultOnly = true - } - list, err := dockerCli.ContextStore().List() - if err != nil { - return nil, err - } - for _, l := range list { - if l.Name == instance { - return nil, errors.Errorf("use `docker --context=%s buildx` to switch to context %s", instance, instance) - } - } - - if instance != "" { - return getInstanceByName(ctx, dockerCli, instance, contextPathHash) - } - return getDefaultDrivers(ctx, dockerCli, defaultOnly, contextPathHash) -} - -func getInstanceByName(ctx context.Context, dockerCli command.Cli, instance, contextPathHash string) ([]build.DriverInfo, error) { - txn, release, err := storeutil.GetStore(dockerCli) - if err != nil { - return nil, err - } - defer release() - - ng, err := txn.NodeGroupByName(instance) - if err != nil { - return nil, err - } - return driversForNodeGroup(ctx, dockerCli, ng, contextPathHash) -} - -// getDefaultDrivers returns drivers based on current cli config -func getDefaultDrivers(ctx context.Context, dockerCli command.Cli, defaultOnly bool, contextPathHash string) ([]build.DriverInfo, error) { - txn, release, err := storeutil.GetStore(dockerCli) - if err != nil { - return nil, err - } - defer release() - - if !defaultOnly { - ng, err := storeutil.GetCurrentInstance(txn, dockerCli) - if err != nil { - return nil, err - } - - if ng != nil { - return driversForNodeGroup(ctx, dockerCli, ng, contextPathHash) - } - } - - imageopt, err := storeutil.GetImageConfig(dockerCli, nil) - if err != nil { - return nil, err - } - - d, err := driver.GetDriver(ctx, "buildx_buildkit_default", nil, dockerCli.Client(), imageopt.Auth, nil, nil, nil, nil, nil, contextPathHash) - if err != nil { - return nil, err - } - return []build.DriverInfo{ - { - Name: "default", - Driver: d, - ImageOpt: imageopt, - }, - }, nil -} - -func loadInfoData(ctx context.Context, d *dinfo) error { - if d.di.Driver == nil { - return nil - } - info, err := d.di.Driver.Info(ctx) - if err != nil { - return err - } - d.info = info - if info.Status == driver.Running { - c, err := d.di.Driver.Client(ctx) - if err != nil { - return err - } - workers, err := c.ListWorkers(ctx) - if err != nil { - return errors.Wrap(err, "listing workers") - } - for _, w := range workers { - d.platforms = append(d.platforms, w.Platforms...) - } - d.platforms = platformutil.Dedupe(d.platforms) - } - return nil -} - -func loadNodeGroupData(ctx context.Context, dockerCli command.Cli, ngi *nginfo) error { - eg, _ := errgroup.WithContext(ctx) - - dis, err := driversForNodeGroup(ctx, dockerCli, ngi.ng, "") - if err != nil { - return err - } - ngi.drivers = make([]dinfo, len(dis)) - for i, di := range dis { - d := di - ngi.drivers[i].di = &d - func(d *dinfo) { - eg.Go(func() error { - if err := loadInfoData(ctx, d); err != nil { - d.err = err - } - return nil - }) - }(&ngi.drivers[i]) - } - - if eg.Wait(); err != nil { - return err - } - - kubernetesDriverCount := 0 - - for _, di := range ngi.drivers { - if di.info != nil && len(di.info.DynamicNodes) > 0 { - kubernetesDriverCount++ - } - } - - isAllKubernetesDrivers := len(ngi.drivers) == kubernetesDriverCount - - if isAllKubernetesDrivers { - var drivers []dinfo - var dynamicNodes []store.Node - - for _, di := range ngi.drivers { - // dynamic nodes are used in Kubernetes driver. - // Kubernetes pods are dynamically mapped to BuildKit Nodes. - if di.info != nil && len(di.info.DynamicNodes) > 0 { - for i := 0; i < len(di.info.DynamicNodes); i++ { - // all []dinfo share *build.DriverInfo and *driver.Info - diClone := di - if pl := di.info.DynamicNodes[i].Platforms; len(pl) > 0 { - diClone.platforms = pl - } - drivers = append(drivers, di) - } - dynamicNodes = append(dynamicNodes, di.info.DynamicNodes...) - } - } - - // not append (remove the static nodes in the store) - ngi.ng.Nodes = dynamicNodes - ngi.drivers = drivers - ngi.ng.Dynamic = true - } - - return nil -} - -func dockerAPI(dockerCli command.Cli) *api { - return &api{dockerCli: dockerCli} -} - -type api struct { - dockerCli command.Cli -} - -func (a *api) DockerAPI(name string) (dockerclient.APIClient, error) { - if name == "" { - name = a.dockerCli.CurrentContext() - } - return clientForEndpoint(a.dockerCli, name) -} - -type dinfo struct { - di *build.DriverInfo - info *driver.Info - platforms []specs.Platform - err error -} - -type nginfo struct { - ng *store.NodeGroup - drivers []dinfo - err error -} - -func boot(ctx context.Context, ngi *nginfo) (bool, error) { - toBoot := make([]int, 0, len(ngi.drivers)) - for i, d := range ngi.drivers { - if d.err != nil || d.di.Err != nil || d.di.Driver == nil || d.info == nil { - continue - } - if d.info.Status != driver.Running { - toBoot = append(toBoot, i) - } - } - if len(toBoot) == 0 { - return false, nil - } - - printer := progress.NewPrinter(context.TODO(), os.Stderr, "auto") - - baseCtx := ctx - eg, _ := errgroup.WithContext(ctx) - for _, idx := range toBoot { - func(idx int) { - eg.Go(func() error { - pw := progress.WithPrefix(printer, ngi.ng.Nodes[idx].Name, len(toBoot) > 1) - _, err := driver.Boot(ctx, baseCtx, ngi.drivers[idx].di.Driver, pw) - if err != nil { - ngi.drivers[idx].err = err - } - return nil - }) - }(idx) - } - - err := eg.Wait() - err1 := printer.Wait() - if err == nil { - err = err1 - } - - return true, err -} diff --git a/driver/kubernetes/config/config.go b/driver/kubernetes/config/config.go new file mode 100644 index 00000000000..0d643fcaff4 --- /dev/null +++ b/driver/kubernetes/config/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "net/url" + "os" + "strings" + + cxtkubernetes "github.com/docker/cli/cli/context/kubernetes" + ctxstore "github.com/docker/cli/cli/context/store" + "k8s.io/client-go/tools/clientcmd" +) + +// FromContext loads k8s config from context +func FromContext(endpointName string, s ctxstore.Reader) (clientcmd.ClientConfig, error) { + if strings.HasPrefix(endpointName, "kubernetes://") { + u, _ := url.Parse(endpointName) + if kubeconfig := u.Query().Get("kubeconfig"); kubeconfig != "" { + _ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfig) + } + rules := clientcmd.NewDefaultClientConfigLoadingRules() + apiConfig, err := rules.Load() + if err != nil { + return nil, err + } + return clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}), nil + } + return cxtkubernetes.ConfigFromContext(endpointName, s) +} diff --git a/store/nodegroup.go b/store/nodegroup.go index c9f97a67228..b882884070b 100644 --- a/store/nodegroup.go +++ b/store/nodegroup.go @@ -15,6 +15,7 @@ type NodeGroup struct { Driver string Nodes []Node Dynamic bool + Current bool } type Node struct { diff --git a/store/storeutil/docker.go b/store/storeutil/docker.go new file mode 100644 index 00000000000..1f5bf27ecad --- /dev/null +++ b/store/storeutil/docker.go @@ -0,0 +1,119 @@ +package storeutil + +import ( + "context" + "io" + "sync" + + "github.com/docker/buildx/util/progress" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/docker/client" +) + +// DockerClient represents an active docker object. +type DockerClient struct { + cli command.Cli +} + +// NewDockerClient initializes a new docker client. +func NewDockerClient(cli command.Cli) *DockerClient { + return &DockerClient{cli: cli} +} + +// API returns a new docker API client. +func (c *DockerClient) API(name string) (client.APIClient, error) { + if name == "" { + name = c.cli.CurrentContext() + } + return ClientForEndpoint(c.cli, name) +} + +// LoadImage imports an image to docker. +func (c *DockerClient) LoadImage(ctx context.Context, name string, status progress.Writer) (io.WriteCloser, func(), error) { + dapi, err := c.API(name) + if err != nil { + return nil, nil, err + } + + pr, pw := io.Pipe() + done := make(chan struct{}) + + ctx, cancel := context.WithCancel(ctx) + var w *waitingWriter + w = &waitingWriter{ + PipeWriter: pw, + f: func() { + resp, err := dapi.ImageLoad(ctx, pr, false) + defer close(done) + if err != nil { + pr.CloseWithError(err) + w.mu.Lock() + w.err = err + w.mu.Unlock() + return + } + prog := progress.WithPrefix(status, "", false) + progress.FromReader(prog, "importing to docker", resp.Body) + }, + done: done, + cancel: cancel, + } + return w, func() { + pr.Close() + }, nil +} + +type waitingWriter struct { + *io.PipeWriter + f func() + once sync.Once + mu sync.Mutex + err error + done chan struct{} + cancel func() +} + +func (w *waitingWriter) Write(dt []byte) (int, error) { + w.once.Do(func() { + go w.f() + }) + return w.PipeWriter.Write(dt) +} + +func (w *waitingWriter) Close() error { + err := w.PipeWriter.Close() + <-w.done + if err == nil { + w.mu.Lock() + defer w.mu.Unlock() + return w.err + } + return err +} + +// ClientForEndpoint returns a docker client for an endpoint +func ClientForEndpoint(dockerCli command.Cli, name string) (client.APIClient, error) { + dem, err := GetDockerEndpoint(dockerCli, name) + if err == nil && dem != nil { + ep, err := docker.WithTLSData(dockerCli.ContextStore(), name, *dem) + if err != nil { + return nil, err + } + clientOpts, err := ep.ClientOpts() + if err != nil { + return nil, err + } + return client.NewClientWithOpts(append(clientOpts, client.WithAPIVersionNegotiation())...) + } + ep := docker.Endpoint{ + EndpointMeta: docker.EndpointMeta{ + Host: name, + }, + } + clientOpts, err := ep.ClientOpts() + if err != nil { + return nil, err + } + return client.NewClientWithOpts(append(clientOpts, client.WithAPIVersionNegotiation())...) +} diff --git a/store/storeutil/storeutil.go b/store/storeutil/storeutil.go index b87e3f488aa..95a951036bf 100644 --- a/store/storeutil/storeutil.go +++ b/store/storeutil/storeutil.go @@ -30,33 +30,35 @@ func GetCurrentEndpoint(dockerCli command.Cli) (string, error) { if name != "default" { return name, nil } - de, err := GetDockerEndpoint(dockerCli, name) + dem, err := GetDockerEndpoint(dockerCli, name) if err != nil { return "", errors.Errorf("docker endpoint for %q not found", name) + } else if dem != nil { + return dem.Host, nil } - return de, nil + return "", nil } -// GetDockerEndpoint returns docker endpoint string for given context -func GetDockerEndpoint(dockerCli command.Cli, name string) (string, error) { +// GetDockerEndpoint returns docker endpoint meta for given context +func GetDockerEndpoint(dockerCli command.Cli, name string) (*docker.EndpointMeta, error) { list, err := dockerCli.ContextStore().List() if err != nil { - return "", err + return nil, err } for _, l := range list { if l.Name == name { ep, ok := l.Endpoints["docker"] if !ok { - return "", errors.Errorf("context %q does not have a Docker endpoint", name) + return nil, errors.Errorf("context %q does not have a Docker endpoint", name) } typed, ok := ep.(docker.EndpointMeta) if !ok { - return "", errors.Errorf("endpoint %q is not of type EndpointMeta, %T", ep, ep) + return nil, errors.Errorf("endpoint %q is not of type EndpointMeta, %T", ep, ep) } - return typed.Host, nil + return &typed, nil } } - return "", nil + return nil, nil } // GetCurrentInstance finds the current builder instance @@ -73,6 +75,7 @@ func GetCurrentInstance(txn *store.Txn, dockerCli command.Cli) (*store.NodeGroup ng, _ = GetNodeGroup(txn, dockerCli, dockerCli.CurrentContext()) } + ng.Current = true return ng, nil } diff --git a/util/builderutil/builder.go b/util/builderutil/builder.go new file mode 100644 index 00000000000..3b4d07ba979 --- /dev/null +++ b/util/builderutil/builder.go @@ -0,0 +1,173 @@ +package builderutil + +import ( + "context" + "os" + + "github.com/docker/buildx/driver" + "github.com/docker/buildx/store" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/imagetools" + "github.com/docker/buildx/util/progress" + "github.com/docker/cli/cli/command" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +// Builder represents an active builder object +type Builder struct { + dockerCli command.Cli + txn *store.Txn + + NodeGroup *store.NodeGroup + Drivers []Driver + Err error +} + +// New initializes a new builder client +func New(dockerCli command.Cli, txn *store.Txn, name string) (_ *Builder, err error) { + b := &Builder{ + dockerCli: dockerCli, + txn: txn, + } + + if name != "" { + b.NodeGroup, err = storeutil.GetNodeGroup(txn, dockerCli, name) + if err != nil { + return nil, err + } + return b, nil + } + + b.NodeGroup, err = storeutil.GetCurrentInstance(txn, dockerCli) + if err != nil { + return nil, err + } + return b, nil +} + +// Validate validates builder context +func (b *Builder) Validate() error { + if b.NodeGroup.Name == "default" && b.NodeGroup.Name != b.dockerCli.CurrentContext() { + return errors.Errorf("use `docker --context=default buildx` to switch to default context") + } + list, err := b.dockerCli.ContextStore().List() + if err != nil { + return err + } + for _, l := range list { + if l.Name == b.NodeGroup.Name && b.NodeGroup.Name != "default" { + return errors.Errorf("use `docker --context=%s buildx` to switch to context %q", b.NodeGroup.Name, b.NodeGroup.Name) + } + } + return nil +} + +// GetImageOpt returns registry auth configuration +func (b *Builder) GetImageOpt() (imagetools.Opt, error) { + return storeutil.GetImageConfig(b.dockerCli, b.NodeGroup) +} + +// Boot bootstrap a builder +func (b *Builder) Boot(ctx context.Context) (bool, error) { + toBoot := make([]int, 0, len(b.Drivers)) + for idx, d := range b.Drivers { + if d.Err != nil || d.Driver == nil || d.Info == nil { + continue + } + if d.Info.Status != driver.Running { + toBoot = append(toBoot, idx) + } + } + if len(toBoot) == 0 { + return false, nil + } + + printer := progress.NewPrinter(context.Background(), os.Stderr, progress.PrinterModeAuto) + + baseCtx := ctx + eg, _ := errgroup.WithContext(ctx) + for _, idx := range toBoot { + func(idx int) { + eg.Go(func() error { + pw := progress.WithPrefix(printer, b.NodeGroup.Nodes[idx].Name, len(toBoot) > 1) + _, err := driver.Boot(ctx, baseCtx, b.Drivers[idx].Driver, pw) + if err != nil { + b.Drivers[idx].Err = err + } + return nil + }) + }(idx) + } + + err := eg.Wait() + err1 := printer.Wait() + if err == nil { + err = err1 + } + + return true, err +} + +// GetBuilders returns all builders +func GetBuilders(dockerCli command.Cli, txn *store.Txn) ([]*Builder, error) { + storeng, err := txn.List() + if err != nil { + return nil, err + } + + currentName := "default" + current, err := storeutil.GetCurrentInstance(txn, dockerCli) + if err != nil { + return nil, err + } + if current != nil { + currentName = current.Name + if current.Name == "default" { + currentName = current.Nodes[0].Endpoint + } + } + + currentSet := false + storeBuilders := make([]*Builder, len(storeng)) + for i, ng := range storeng { + if !currentSet && ng.Name == currentName { + ng.Current = true + currentSet = true + } + storeBuilders[i] = &Builder{ + dockerCli: dockerCli, + txn: txn, + NodeGroup: ng, + } + } + + list, err := dockerCli.ContextStore().List() + if err != nil { + return nil, err + } + ctxBuilders := make([]*Builder, len(list)) + for i, l := range list { + defaultNg := false + if !currentSet && l.Name == currentName { + defaultNg = true + currentSet = true + } + ctxBuilders[i] = &Builder{ + dockerCli: dockerCli, + txn: txn, + NodeGroup: &store.NodeGroup{ + Name: l.Name, + Current: defaultNg, + Nodes: []store.Node{ + { + Name: l.Name, + Endpoint: l.Name, + }, + }, + }, + } + } + + return append(storeBuilders, ctxBuilders...), nil +} diff --git a/util/builderutil/driver.go b/util/builderutil/driver.go new file mode 100644 index 00000000000..3f72e5417d8 --- /dev/null +++ b/util/builderutil/driver.go @@ -0,0 +1,185 @@ +package builderutil + +import ( + "context" + + "github.com/docker/buildx/driver" + k8sconfig "github.com/docker/buildx/driver/kubernetes/config" + "github.com/docker/buildx/store" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/imagetools" + "github.com/docker/buildx/util/platformutil" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +type Driver struct { + Name string + Driver driver.Driver + Info *driver.Info + Platforms []ocispecs.Platform + ImageOpt imagetools.Opt + Err error +} + +// LoadDrivers loads drivers. +func (b *Builder) LoadDrivers(ctx context.Context, withData bool, contextPathHash string) error { + eg, _ := errgroup.WithContext(ctx) + b.Drivers = make([]Driver, len(b.NodeGroup.Nodes)) + + var f driver.Factory + if b.NodeGroup.Driver != "" { + f = driver.GetFactory(b.NodeGroup.Driver, true) + if f == nil { + return errors.Errorf("failed to find driver %q", f) + } + } else { + dockerapi, err := storeutil.ClientForEndpoint(b.dockerCli, b.NodeGroup.Nodes[0].Endpoint) + if err != nil { + return err + } + f, err = driver.GetDefaultFactory(ctx, dockerapi, false) + if err != nil { + return err + } + b.NodeGroup.Driver = f.Name() + } + + imageopt, err := b.GetImageOpt() + if err != nil { + return err + } + + for i, n := range b.NodeGroup.Nodes { + func(i int, n store.Node) { + eg.Go(func() error { + di := Driver{ + Name: n.Name, + Platforms: n.Platforms, + } + defer func() { + b.Drivers[i] = di + }() + + dockerapi, err := storeutil.ClientForEndpoint(b.dockerCli, n.Endpoint) + if err != nil { + di.Err = err + return nil + } + + contextStore := b.dockerCli.ContextStore() + + var kcc driver.KubeClientConfig + kcc, err = k8sconfig.FromContext(n.Endpoint, contextStore) + if err != nil { + // err is returned if n.Endpoint is non-context name like "unix:///var/run/docker.sock". + // try again with name="default". + // FIXME(@AkihiroSuda): n should retain real context name. + kcc, err = k8sconfig.FromContext("default", contextStore) + if err != nil { + logrus.Error(err) + } + } + + tryToUseKubeConfigInCluster := false + if kcc == nil { + tryToUseKubeConfigInCluster = true + } else { + if _, err := kcc.ClientConfig(); err != nil { + tryToUseKubeConfigInCluster = true + } + } + if tryToUseKubeConfigInCluster { + kccInCluster := driver.KubeClientConfigInCluster{} + if _, err := kccInCluster.ClientConfig(); err == nil { + logrus.Debug("using kube config in cluster") + kcc = kccInCluster + } + } + + d, err := driver.GetDriver(ctx, "buildx_buildkit_"+n.Name, f, dockerapi, imageopt.Auth, kcc, n.Flags, n.Files, n.DriverOpts, n.Platforms, contextPathHash) + if err != nil { + di.Err = err + return nil + } + di.Driver = d + di.ImageOpt = imageopt + + if withData { + if err := di.loadData(ctx); err != nil { + di.Err = err + } + } + return nil + }) + }(i, n) + } + if err := eg.Wait(); err != nil { + return err + } + + if withData { + kubernetesDriverCount := 0 + for _, d := range b.Drivers { + if d.Info != nil && len(d.Info.DynamicNodes) > 0 { + kubernetesDriverCount++ + } + } + + isAllKubernetesDrivers := len(b.Drivers) == kubernetesDriverCount + if isAllKubernetesDrivers { + var drivers []Driver + var dynamicNodes []store.Node + for _, di := range b.Drivers { + // dynamic nodes are used in Kubernetes driver. + // Kubernetes pods are dynamically mapped to BuildKit Nodes. + if di.Info != nil && len(di.Info.DynamicNodes) > 0 { + for i := 0; i < len(di.Info.DynamicNodes); i++ { + // all []dinfo share *build.DriverInfo and *driver.Info + diClone := di + if pl := di.Info.DynamicNodes[i].Platforms; len(pl) > 0 { + diClone.Platforms = pl + } + drivers = append(drivers, di) + } + dynamicNodes = append(dynamicNodes, di.Info.DynamicNodes...) + } + } + + // not append (remove the static nodes in the store) + b.NodeGroup.Nodes = dynamicNodes + b.Drivers = drivers + b.NodeGroup.Dynamic = true + } + } + + return nil +} + +func (d *Driver) loadData(ctx context.Context) error { + if d.Driver == nil { + return nil + } + info, err := d.Driver.Info(ctx) + if err != nil { + return err + } + d.Info = info + if d.Info.Status == driver.Running { + cdriver, err := d.Driver.Client(ctx) + if err != nil { + return err + } + workers, err := cdriver.ListWorkers(ctx) + if err != nil { + return errors.Wrap(err, "listing workers") + } + for _, w := range workers { + d.Platforms = append(d.Platforms, w.Platforms...) + } + d.Platforms = platformutil.Dedupe(d.Platforms) + } + return nil +}