From a9fd1289104299a5218f341da7892e4989ee4d82 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 31 Jan 2023 16:39:57 +0000 Subject: [PATCH] controller: move controllers out of commands into separate package Signed-off-by: Justin Chadwell --- commands/bake.go | 2 +- commands/build.go | 393 +-------------- commands/controller/pb/generate.go | 3 - commands/controllerremote_nolinux.go | 18 - commands/root.go | 3 +- controller/build/build.go | 447 ++++++++++++++++++ {commands => controller/build}/print.go | 2 +- controller/control/controller.go | 24 + controller/controller.go | 27 ++ .../local/controller.go | 11 +- .../pb/controller.pb.go | 0 .../pb/controller.proto | 0 controller/pb/generate.go | 3 + .../remote}/client.go | 4 +- .../remote/controller.go | 35 +- controller/remote/controller_nolinux.go | 18 + .../controller => controller/remote}/io.go | 4 +- .../remote/server.go | 26 +- monitor/monitor.go | 14 +- 19 files changed, 580 insertions(+), 454 deletions(-) delete mode 100644 commands/controller/pb/generate.go delete mode 100644 commands/controllerremote_nolinux.go create mode 100644 controller/build/build.go rename {commands => controller/build}/print.go (98%) create mode 100644 controller/control/controller.go create mode 100644 controller/controller.go rename commands/controllerlocal.go => controller/local/controller.go (82%) rename {commands/controller => controller}/pb/controller.pb.go (100%) rename {commands/controller => controller}/pb/controller.proto (100%) create mode 100644 controller/pb/generate.go rename {commands/controller => controller/remote}/client.go (98%) rename commands/controllerremote.go => controller/remote/controller.go (87%) create mode 100644 controller/remote/controller_nolinux.go rename {commands/controller => controller/remote}/io.go (99%) rename commands/controller/controller.go => controller/remote/server.go (91%) diff --git a/commands/bake.go b/commands/bake.go index 98ca9805..5ad2b995 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -10,7 +10,7 @@ import ( "github.com/docker/buildx/bake" "github.com/docker/buildx/build" "github.com/docker/buildx/builder" - controllerapi "github.com/docker/buildx/commands/controller/pb" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/dockerutil" diff --git a/commands/build.go b/commands/build.go index bfc8fe8a..949da2da 100644 --- a/commands/build.go +++ b/commands/build.go @@ -1,7 +1,6 @@ package commands import ( - "bytes" "context" "encoding/base64" "encoding/csv" @@ -9,40 +8,28 @@ import ( "fmt" "io" "os" - "path/filepath" "runtime" "strconv" "strings" - "sync" "github.com/containerd/console" - "github.com/docker/buildx/build" - "github.com/docker/buildx/builder" - controllerapi "github.com/docker/buildx/commands/controller/pb" + "github.com/docker/buildx/controller" + cbuild "github.com/docker/buildx/controller/build" + "github.com/docker/buildx/controller/control" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/monitor" "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" - "github.com/docker/buildx/util/buildflags" - "github.com/docker/buildx/util/confutil" - "github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/ioset" - "github.com/docker/buildx/util/platformutil" - "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/tracing" "github.com/docker/cli-docs-tool/annotation" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/config" dockeropts "github.com/docker/cli/opts" - "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/ioutils" "github.com/docker/go-units" - "github.com/moby/buildkit/client" - "github.com/moby/buildkit/session/auth/authprovider" - "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/util/appcontext" "github.com/moby/buildkit/util/grpcerrors" - "github.com/morikuni/aec" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -50,15 +37,11 @@ import ( "google.golang.org/grpc/codes" ) -const defaultTargetName = "default" - type buildOptions struct { - progress string - invoke string - serverConfig string - root string - detach bool + progress string + invoke string controllerapi.BuildOptions + control.ControlOptions } func runBuild(dockerCli command.Cli, in buildOptions) error { @@ -72,266 +55,10 @@ func runBuild(dockerCli command.Cli, in buildOptions) error { end(err) }() - _, err = runBuildWithContext(ctx, dockerCli, in.BuildOptions, os.Stdin, in.progress, nil) + _, err = cbuild.RunBuild(ctx, dockerCli, in.BuildOptions, os.Stdin, in.progress, nil) return err } -func runBuildWithContext(ctx context.Context, dockerCli command.Cli, in controllerapi.BuildOptions, inStream io.Reader, progressMode string, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) { - if in.Opts.NoCache && len(in.NoCacheFilter) > 0 { - return nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together") - } - - if in.Quiet && progressMode != progress.PrinterModeAuto && progressMode != progress.PrinterModeQuiet { - return nil, errors.Errorf("progress=%s and quiet cannot be used together", progressMode) - } else if in.Quiet { - progressMode = "quiet" - } - - contexts, err := parseContextNames(in.Contexts) - if err != nil { - return nil, err - } - - printFunc, err := parsePrintFunc(in.PrintFunc) - if err != nil { - return nil, err - } - - opts := build.Options{ - Inputs: build.Inputs{ - ContextPath: in.ContextPath, - DockerfilePath: in.DockerfileName, - InStream: inStream, - NamedContexts: contexts, - }, - BuildArgs: listToMap(in.BuildArgs, true), - ExtraHosts: in.ExtraHosts, - ImageIDFile: in.ImageIDFile, - Labels: listToMap(in.Labels, false), - NetworkMode: in.NetworkMode, - NoCache: in.Opts.NoCache, - NoCacheFilter: in.NoCacheFilter, - Pull: in.Opts.Pull, - ShmSize: dockeropts.MemBytes(in.ShmSize), - Tags: in.Tags, - Target: in.Target, - Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits), - PrintFunc: printFunc, - } - - platforms, err := platformutil.Parse(in.Platforms) - if err != nil { - return nil, err - } - opts.Platforms = platforms - - dockerConfig := config.LoadDefaultConfigFile(os.Stderr) - opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(dockerConfig)) - - secrets, err := buildflags.ParseSecretSpecs(in.Secrets) - if err != nil { - return nil, err - } - opts.Session = append(opts.Session, secrets) - - sshSpecs := in.SSH - if len(sshSpecs) == 0 && buildflags.IsGitSSH(in.ContextPath) { - sshSpecs = []string{"default"} - } - ssh, err := buildflags.ParseSSHSpecs(sshSpecs) - if err != nil { - return nil, err - } - opts.Session = append(opts.Session, ssh) - - outputs, err := buildflags.ParseOutputs(in.Outputs) - if err != nil { - return nil, err - } - if in.Opts.ExportPush { - if in.Opts.ExportLoad { - return nil, errors.Errorf("push and load may not be set together at the moment") - } - if len(outputs) == 0 { - outputs = []client.ExportEntry{{ - Type: "image", - Attrs: map[string]string{ - "push": "true", - }, - }} - } else { - switch outputs[0].Type { - case "image": - outputs[0].Attrs["push"] = "true" - default: - return nil, errors.Errorf("push and %q output can't be used together", outputs[0].Type) - } - } - } - if in.Opts.ExportLoad { - if len(outputs) == 0 { - outputs = []client.ExportEntry{{ - Type: "docker", - Attrs: map[string]string{}, - }} - } else { - switch outputs[0].Type { - case "docker": - default: - return nil, errors.Errorf("load and %q output can't be used together", outputs[0].Type) - } - } - } - opts.Exports = outputs - - inAttests := append([]string{}, in.Attests...) - if in.Opts.Provenance != "" { - inAttests = append(inAttests, buildflags.CanonicalizeAttest("provenance", in.Opts.Provenance)) - } - if in.Opts.SBOM != "" { - inAttests = append(inAttests, buildflags.CanonicalizeAttest("sbom", in.Opts.SBOM)) - } - opts.Attests, err = buildflags.ParseAttests(inAttests) - if err != nil { - return nil, err - } - - cacheImports, err := buildflags.ParseCacheEntry(in.CacheFrom) - if err != nil { - return nil, err - } - opts.CacheFrom = cacheImports - - cacheExports, err := buildflags.ParseCacheEntry(in.CacheTo) - if err != nil { - return nil, err - } - opts.CacheTo = cacheExports - - allow, err := buildflags.ParseEntitlements(in.Allow) - if err != nil { - return nil, err - } - opts.Allow = allow - - // key string used for kubernetes "sticky" mode - contextPathHash, err := filepath.Abs(in.ContextPath) - if err != nil { - contextPathHash = in.ContextPath - } - - b, err := builder.New(dockerCli, - builder.WithName(in.Opts.Builder), - builder.WithContextPathHash(contextPathHash), - ) - if err != nil { - return nil, err - } - if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil { - return nil, errors.Wrapf(err, "failed to update builder last activity time") - } - nodes, err := b.LoadNodes(ctx, false) - if err != nil { - return nil, err - } - - imageID, res, err := buildTargets(ctx, dockerCli, nodes, map[string]build.Options{defaultTargetName: opts}, progressMode, in.Opts.MetadataFile, statusChan) - err = wrapBuildError(err, false) - if err != nil { - return nil, err - } - - if in.Quiet { - fmt.Println(imageID) - } - return res, nil -} - -func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.Node, opts map[string]build.Options, progressMode string, metadataFile string, statusChan chan *client.SolveStatus) (imageID string, res *build.ResultContext, err error) { - ctx2, cancel := context.WithCancel(context.TODO()) - defer cancel() - - printer, err := progress.NewPrinter(ctx2, os.Stderr, os.Stderr, progressMode) - if err != nil { - return "", nil, err - } - - var mu sync.Mutex - var idx int - resp, err := build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress.Tee(printer, statusChan), func(driverIndex int, gotRes *build.ResultContext) { - mu.Lock() - defer mu.Unlock() - if res == nil || driverIndex < idx { - idx, res = driverIndex, gotRes - } - }) - err1 := printer.Wait() - if err == nil { - err = err1 - } - if err != nil { - return "", nil, err - } - - if len(metadataFile) > 0 && resp != nil { - if err := writeMetadataFile(metadataFile, decodeExporterResponse(resp[defaultTargetName].ExporterResponse)); err != nil { - return "", nil, err - } - } - - printWarnings(os.Stderr, printer.Warnings(), progressMode) - - for k := range resp { - if opts[k].PrintFunc != nil { - if err := printResult(opts[k].PrintFunc, resp[k].ExporterResponse); err != nil { - return "", nil, err - } - } - } - - return resp[defaultTargetName].ExporterResponse["containerimage.digest"], res, err -} - -func printWarnings(w io.Writer, warnings []client.VertexWarning, mode string) { - if len(warnings) == 0 || mode == progress.PrinterModeQuiet { - return - } - fmt.Fprintf(w, "\n ") - sb := &bytes.Buffer{} - if len(warnings) == 1 { - fmt.Fprintf(sb, "1 warning found") - } else { - fmt.Fprintf(sb, "%d warnings found", len(warnings)) - } - if logrus.GetLevel() < logrus.DebugLevel { - fmt.Fprintf(sb, " (use --debug to expand)") - } - fmt.Fprintf(sb, ":\n") - fmt.Fprint(w, aec.Apply(sb.String(), aec.YellowF)) - - for _, warn := range warnings { - fmt.Fprintf(w, " - %s\n", warn.Short) - if logrus.GetLevel() < logrus.DebugLevel { - continue - } - for _, d := range warn.Detail { - fmt.Fprintf(w, "%s\n", d) - } - if warn.URL != "" { - fmt.Fprintf(w, "More info: %s\n", warn.URL) - } - if warn.SourceInfo != nil && warn.Range != nil { - src := errdefs.Source{ - Info: warn.SourceInfo, - Ranges: warn.Range, - } - src.Print(w) - } - fmt.Fprintf(w, "\n") - - } -} - func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { options := newBuildOptions() cFlags := &commonFlags{} @@ -430,9 +157,9 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { if isExperimental() { flags.StringVar(&options.invoke, "invoke", "", "Invoke a command after the build [experimental]") - flags.StringVar(&options.root, "root", "", "Specify root directory of server to connect [experimental]") - flags.BoolVar(&options.detach, "detach", runtime.GOOS == "linux", "Detach buildx server (supported only on linux) [experimental]") - flags.StringVar(&options.serverConfig, "server-config", "", "Specify buildx server config file (used only when launching new server) [experimental]") + flags.StringVar(&options.Root, "root", "", "Specify root directory of server to connect [experimental]") + flags.BoolVar(&options.Detach, "detach", runtime.GOOS == "linux", "Detach buildx server (supported only on linux) [experimental]") + flags.StringVar(&options.ServerConfig, "server-config", "", "Specify buildx server config file (used only when launching new server) [experimental]") } // hidden flags @@ -514,74 +241,6 @@ func checkWarnedFlags(f *pflag.Flag) { } } -func listToMap(values []string, defaultEnv bool) map[string]string { - result := make(map[string]string, len(values)) - for _, value := range values { - kv := strings.SplitN(value, "=", 2) - if len(kv) == 1 { - if defaultEnv { - v, ok := os.LookupEnv(kv[0]) - if ok { - result[kv[0]] = v - } - } else { - result[kv[0]] = "" - } - } else { - result[kv[0]] = kv[1] - } - } - return result -} - -func parseContextNames(values []string) (map[string]build.NamedContext, error) { - if len(values) == 0 { - return nil, nil - } - result := make(map[string]build.NamedContext, len(values)) - for _, value := range values { - kv := strings.SplitN(value, "=", 2) - if len(kv) != 2 { - return nil, errors.Errorf("invalid context value: %s, expected key=value", value) - } - named, err := reference.ParseNormalizedNamed(kv[0]) - if err != nil { - return nil, errors.Wrapf(err, "invalid context name %s", kv[0]) - } - name := strings.TrimSuffix(reference.FamiliarString(named), ":latest") - result[name] = build.NamedContext{Path: kv[1]} - } - return result, nil -} - -func parsePrintFunc(str string) (*build.PrintFunc, error) { - if str == "" { - return nil, nil - } - csvReader := csv.NewReader(strings.NewReader(str)) - fields, err := csvReader.Read() - if err != nil { - return nil, err - } - f := &build.PrintFunc{} - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - if len(parts) == 2 { - if parts[0] == "format" { - f.Format = parts[1] - } else { - return nil, errors.Errorf("invalid print field: %s", field) - } - } else { - if f.Name != "" { - return nil, errors.Errorf("invalid print value: %s", str) - } - f.Name = field - } - } - return f, nil -} - func writeMetadataFile(filename string, dt interface{}) error { b, err := json.MarshalIndent(dt, "", " ") if err != nil { @@ -675,20 +334,11 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er if err != nil { return err } - } - var c monitor.BuildxController - var err error - if options.detach { - logrus.Infof("connecting to buildx server") - c, err = newRemoteBuildxController(ctx, dockerCli, options) - if err != nil { - return fmt.Errorf("failed to use buildx server; use --detach=false: %w", err) - } - } else { - logrus.Infof("launching local buildx controller") - c = newLocalBuildxController(ctx, dockerCli) + c, err := controller.NewController(ctx, options.ControlOptions, dockerCli) + if err != nil { + return err } defer func() { if err := c.Close(); err != nil { @@ -800,21 +450,6 @@ func parseInvokeConfig(invoke string) (cfg controllerapi.ContainerConfig, err er return cfg, nil } -func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.UlimitOpt { - if u == nil { - return nil - } - values := make(map[string]*units.Ulimit) - for k, v := range u.Values { - values[k] = &units.Ulimit{ - Name: v.Name, - Hard: v.Hard, - Soft: v.Soft, - } - } - return dockeropts.NewUlimitOpt(&values) -} - func newBuildOptions() buildOptions { return buildOptions{ BuildOptions: controllerapi.BuildOptions{ diff --git a/commands/controller/pb/generate.go b/commands/controller/pb/generate.go deleted file mode 100644 index d5f1b822..00000000 --- a/commands/controller/pb/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package pb - -//go:generate protoc -I=. -I=../../../vendor/ --gogo_out=plugins=grpc:. controller.proto diff --git a/commands/controllerremote_nolinux.go b/commands/controllerremote_nolinux.go deleted file mode 100644 index 0488e919..00000000 --- a/commands/controllerremote_nolinux.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build !linux - -package commands - -import ( - "context" - "fmt" - - "github.com/docker/buildx/monitor" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" -) - -func newRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts buildOptions) (monitor.BuildxController, error) { - return nil, fmt.Errorf("remote buildx unsupported") -} - -func addControllerCommands(cmd *cobra.Command, dockerCli command.Cli, rootOpts *rootOptions) {} diff --git a/commands/root.go b/commands/root.go index 6d806fd9..eda2c1e5 100644 --- a/commands/root.go +++ b/commands/root.go @@ -4,6 +4,7 @@ import ( "os" imagetoolscmd "github.com/docker/buildx/commands/imagetools" + "github.com/docker/buildx/controller/remote" "github.com/docker/buildx/util/logutil" "github.com/docker/cli-docs-tool/annotation" "github.com/docker/cli/cli" @@ -87,7 +88,7 @@ func addCommands(cmd *cobra.Command, dockerCli command.Cli) { imagetoolscmd.RootCmd(dockerCli, imagetoolscmd.RootOptions{Builder: &opts.builder}), ) if isExperimental() { - addControllerCommands(cmd, dockerCli, opts) + remote.AddControllerCommands(cmd, dockerCli) } } diff --git a/controller/build/build.go b/controller/build/build.go new file mode 100644 index 00000000..88cc8257 --- /dev/null +++ b/controller/build/build.go @@ -0,0 +1,447 @@ +package build + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/docker/buildx/build" + "github.com/docker/buildx/builder" + controllerapi "github.com/docker/buildx/controller/pb" + "github.com/docker/buildx/store" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/confutil" + "github.com/docker/buildx/util/dockerutil" + "github.com/docker/buildx/util/platformutil" + "github.com/docker/buildx/util/progress" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config" + dockeropts "github.com/docker/cli/opts" + "github.com/docker/distribution/reference" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/go-units" + "github.com/moby/buildkit/client" + "github.com/moby/buildkit/session/auth/authprovider" + "github.com/moby/buildkit/solver/errdefs" + "github.com/moby/buildkit/util/grpcerrors" + "github.com/morikuni/aec" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" +) + +const defaultTargetName = "default" + +func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.BuildOptions, inStream io.Reader, progressMode string, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) { + if in.Opts.NoCache && len(in.NoCacheFilter) > 0 { + return nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together") + } + + if in.Quiet && progressMode != progress.PrinterModeAuto && progressMode != progress.PrinterModeQuiet { + return nil, errors.Errorf("progress=%s and quiet cannot be used together", progressMode) + } else if in.Quiet { + progressMode = "quiet" + } + + contexts, err := parseContextNames(in.Contexts) + if err != nil { + return nil, err + } + + printFunc, err := parsePrintFunc(in.PrintFunc) + if err != nil { + return nil, err + } + + opts := build.Options{ + Inputs: build.Inputs{ + ContextPath: in.ContextPath, + DockerfilePath: in.DockerfileName, + InStream: inStream, + NamedContexts: contexts, + }, + BuildArgs: listToMap(in.BuildArgs, true), + ExtraHosts: in.ExtraHosts, + ImageIDFile: in.ImageIDFile, + Labels: listToMap(in.Labels, false), + NetworkMode: in.NetworkMode, + NoCache: in.Opts.NoCache, + NoCacheFilter: in.NoCacheFilter, + Pull: in.Opts.Pull, + ShmSize: dockeropts.MemBytes(in.ShmSize), + Tags: in.Tags, + Target: in.Target, + Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits), + PrintFunc: printFunc, + } + + platforms, err := platformutil.Parse(in.Platforms) + if err != nil { + return nil, err + } + opts.Platforms = platforms + + dockerConfig := config.LoadDefaultConfigFile(os.Stderr) + opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(dockerConfig)) + + secrets, err := buildflags.ParseSecretSpecs(in.Secrets) + if err != nil { + return nil, err + } + opts.Session = append(opts.Session, secrets) + + sshSpecs := in.SSH + if len(sshSpecs) == 0 && buildflags.IsGitSSH(in.ContextPath) { + sshSpecs = []string{"default"} + } + ssh, err := buildflags.ParseSSHSpecs(sshSpecs) + if err != nil { + return nil, err + } + opts.Session = append(opts.Session, ssh) + + outputs, err := buildflags.ParseOutputs(in.Outputs) + if err != nil { + return nil, err + } + if in.Opts.ExportPush { + if in.Opts.ExportLoad { + return nil, errors.Errorf("push and load may not be set together at the moment") + } + if len(outputs) == 0 { + outputs = []client.ExportEntry{{ + Type: "image", + Attrs: map[string]string{ + "push": "true", + }, + }} + } else { + switch outputs[0].Type { + case "image": + outputs[0].Attrs["push"] = "true" + default: + return nil, errors.Errorf("push and %q output can't be used together", outputs[0].Type) + } + } + } + if in.Opts.ExportLoad { + if len(outputs) == 0 { + outputs = []client.ExportEntry{{ + Type: "docker", + Attrs: map[string]string{}, + }} + } else { + switch outputs[0].Type { + case "docker": + default: + return nil, errors.Errorf("load and %q output can't be used together", outputs[0].Type) + } + } + } + opts.Exports = outputs + + inAttests := append([]string{}, in.Attests...) + if in.Opts.Provenance != "" { + inAttests = append(inAttests, buildflags.CanonicalizeAttest("provenance", in.Opts.Provenance)) + } + if in.Opts.SBOM != "" { + inAttests = append(inAttests, buildflags.CanonicalizeAttest("sbom", in.Opts.SBOM)) + } + opts.Attests, err = buildflags.ParseAttests(inAttests) + if err != nil { + return nil, err + } + + cacheImports, err := buildflags.ParseCacheEntry(in.CacheFrom) + if err != nil { + return nil, err + } + opts.CacheFrom = cacheImports + + cacheExports, err := buildflags.ParseCacheEntry(in.CacheTo) + if err != nil { + return nil, err + } + opts.CacheTo = cacheExports + + allow, err := buildflags.ParseEntitlements(in.Allow) + if err != nil { + return nil, err + } + opts.Allow = allow + + // key string used for kubernetes "sticky" mode + contextPathHash, err := filepath.Abs(in.ContextPath) + if err != nil { + contextPathHash = in.ContextPath + } + + b, err := builder.New(dockerCli, + builder.WithName(in.Opts.Builder), + builder.WithContextPathHash(contextPathHash), + ) + if err != nil { + return nil, err + } + if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil { + return nil, errors.Wrapf(err, "failed to update builder last activity time") + } + nodes, err := b.LoadNodes(ctx, false) + if err != nil { + return nil, err + } + + imageID, res, err := buildTargets(ctx, dockerCli, nodes, map[string]build.Options{defaultTargetName: opts}, progressMode, in.Opts.MetadataFile, statusChan) + err = wrapBuildError(err, false) + if err != nil { + return nil, err + } + + if in.Quiet { + fmt.Println(imageID) + } + return res, nil +} + +func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.Node, opts map[string]build.Options, progressMode string, metadataFile string, statusChan chan *client.SolveStatus) (imageID string, res *build.ResultContext, err error) { + ctx2, cancel := context.WithCancel(context.TODO()) + defer cancel() + + printer, err := progress.NewPrinter(ctx2, os.Stderr, os.Stderr, progressMode) + if err != nil { + return "", nil, err + } + + var mu sync.Mutex + var idx int + resp, err := build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress.Tee(printer, statusChan), func(driverIndex int, gotRes *build.ResultContext) { + mu.Lock() + defer mu.Unlock() + if res == nil || driverIndex < idx { + idx, res = driverIndex, gotRes + } + }) + err1 := printer.Wait() + if err == nil { + err = err1 + } + if err != nil { + return "", nil, err + } + + if len(metadataFile) > 0 && resp != nil { + if err := writeMetadataFile(metadataFile, decodeExporterResponse(resp[defaultTargetName].ExporterResponse)); err != nil { + return "", nil, err + } + } + + printWarnings(os.Stderr, printer.Warnings(), progressMode) + + for k := range resp { + if opts[k].PrintFunc != nil { + if err := printResult(opts[k].PrintFunc, resp[k].ExporterResponse); err != nil { + return "", nil, err + } + } + } + + return resp[defaultTargetName].ExporterResponse["containerimage.digest"], res, err +} + +func printWarnings(w io.Writer, warnings []client.VertexWarning, mode string) { + if len(warnings) == 0 || mode == progress.PrinterModeQuiet { + return + } + fmt.Fprintf(w, "\n ") + sb := &bytes.Buffer{} + if len(warnings) == 1 { + fmt.Fprintf(sb, "1 warning found") + } else { + fmt.Fprintf(sb, "%d warnings found", len(warnings)) + } + if logrus.GetLevel() < logrus.DebugLevel { + fmt.Fprintf(sb, " (use --debug to expand)") + } + fmt.Fprintf(sb, ":\n") + fmt.Fprint(w, aec.Apply(sb.String(), aec.YellowF)) + + for _, warn := range warnings { + fmt.Fprintf(w, " - %s\n", warn.Short) + if logrus.GetLevel() < logrus.DebugLevel { + continue + } + for _, d := range warn.Detail { + fmt.Fprintf(w, "%s\n", d) + } + if warn.URL != "" { + fmt.Fprintf(w, "More info: %s\n", warn.URL) + } + if warn.SourceInfo != nil && warn.Range != nil { + src := errdefs.Source{ + Info: warn.SourceInfo, + Ranges: warn.Range, + } + src.Print(w) + } + fmt.Fprintf(w, "\n") + + } +} + +func listToMap(values []string, defaultEnv bool) map[string]string { + result := make(map[string]string, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) == 1 { + if defaultEnv { + v, ok := os.LookupEnv(kv[0]) + if ok { + result[kv[0]] = v + } + } else { + result[kv[0]] = "" + } + } else { + result[kv[0]] = kv[1] + } + } + return result +} + +func parseContextNames(values []string) (map[string]build.NamedContext, error) { + if len(values) == 0 { + return nil, nil + } + result := make(map[string]build.NamedContext, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) != 2 { + return nil, errors.Errorf("invalid context value: %s, expected key=value", value) + } + named, err := reference.ParseNormalizedNamed(kv[0]) + if err != nil { + return nil, errors.Wrapf(err, "invalid context name %s", kv[0]) + } + name := strings.TrimSuffix(reference.FamiliarString(named), ":latest") + result[name] = build.NamedContext{Path: kv[1]} + } + return result, nil +} + +func parsePrintFunc(str string) (*build.PrintFunc, error) { + if str == "" { + return nil, nil + } + csvReader := csv.NewReader(strings.NewReader(str)) + fields, err := csvReader.Read() + if err != nil { + return nil, err + } + f := &build.PrintFunc{} + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + if len(parts) == 2 { + if parts[0] == "format" { + f.Format = parts[1] + } else { + return nil, errors.Errorf("invalid print field: %s", field) + } + } else { + if f.Name != "" { + return nil, errors.Errorf("invalid print value: %s", str) + } + f.Name = field + } + } + return f, nil +} + +func writeMetadataFile(filename string, dt interface{}) error { + b, err := json.MarshalIndent(dt, "", " ") + if err != nil { + return err + } + return ioutils.AtomicWriteFile(filename, b, 0644) +} + +func decodeExporterResponse(exporterResponse map[string]string) map[string]interface{} { + out := make(map[string]interface{}) + for k, v := range exporterResponse { + dt, err := base64.StdEncoding.DecodeString(v) + if err != nil { + out[k] = v + continue + } + var raw map[string]interface{} + if err = json.Unmarshal(dt, &raw); err != nil || len(raw) == 0 { + out[k] = v + continue + } + out[k] = json.RawMessage(dt) + } + return out +} + +func wrapBuildError(err error, bake bool) error { + if err == nil { + return nil + } + st, ok := grpcerrors.AsGRPCStatus(err) + if ok { + if st.Code() == codes.Unimplemented && strings.Contains(st.Message(), "unsupported frontend capability moby.buildkit.frontend.contexts") { + msg := "current frontend does not support --build-context." + if bake { + msg = "current frontend does not support defining additional contexts for targets." + } + msg += " Named contexts are supported since Dockerfile v1.4. Use #syntax directive in Dockerfile or update to latest BuildKit." + return &wrapped{err, msg} + } + } + return err +} + +type wrapped struct { + err error + msg string +} + +func (w *wrapped) Error() string { + return w.msg +} + +func (w *wrapped) Unwrap() error { + return w.err +} + +func updateLastActivity(dockerCli command.Cli, ng *store.NodeGroup) error { + txn, release, err := storeutil.GetStore(dockerCli) + if err != nil { + return err + } + defer release() + return txn.UpdateLastActivity(ng) +} + +func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.UlimitOpt { + if u == nil { + return nil + } + values := make(map[string]*units.Ulimit) + for k, v := range u.Values { + values[k] = &units.Ulimit{ + Name: v.Name, + Hard: v.Hard, + Soft: v.Soft, + } + } + return dockeropts.NewUlimitOpt(&values) +} diff --git a/commands/print.go b/controller/build/print.go similarity index 98% rename from commands/print.go rename to controller/build/print.go index 9c7f9460..6817e5e9 100644 --- a/commands/print.go +++ b/controller/build/print.go @@ -1,4 +1,4 @@ -package commands +package build import ( "fmt" diff --git a/controller/control/controller.go b/controller/control/controller.go new file mode 100644 index 00000000..343f2bbf --- /dev/null +++ b/controller/control/controller.go @@ -0,0 +1,24 @@ +package control + +import ( + "context" + "io" + + "github.com/containerd/console" + controllerapi "github.com/docker/buildx/controller/pb" +) + +type BuildxController interface { + Invoke(ctx context.Context, ref string, options controllerapi.ContainerConfig, ioIn io.ReadCloser, ioOut io.WriteCloser, ioErr io.WriteCloser) error + Build(ctx context.Context, options controllerapi.BuildOptions, in io.ReadCloser, w io.Writer, out console.File, progressMode string) (ref string, err error) + Kill(ctx context.Context) error + Close() error + List(ctx context.Context) (res []string, _ error) + Disconnect(ctx context.Context, ref string) error +} + +type ControlOptions struct { + ServerConfig string + Root string + Detach bool +} diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 00000000..197340e3 --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,27 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/docker/buildx/controller/control" + "github.com/docker/buildx/controller/local" + "github.com/docker/buildx/controller/remote" + "github.com/docker/cli/cli/command" + "github.com/sirupsen/logrus" +) + +func NewController(ctx context.Context, opts control.ControlOptions, dockerCli command.Cli) (c control.BuildxController, err error) { + if !opts.Detach { + logrus.Infof("launching local buildx controller") + c = local.NewLocalBuildxController(ctx, dockerCli) + return c, nil + } + + logrus.Infof("connecting to buildx server") + c, err = remote.NewRemoteBuildxController(ctx, dockerCli, opts) + if err != nil { + return nil, fmt.Errorf("failed to use buildx server; use --detach=false: %w", err) + } + return c, nil +} diff --git a/commands/controllerlocal.go b/controller/local/controller.go similarity index 82% rename from commands/controllerlocal.go rename to controller/local/controller.go index ad74b5a1..57037bb9 100644 --- a/commands/controllerlocal.go +++ b/controller/local/controller.go @@ -1,4 +1,4 @@ -package commands +package local import ( "context" @@ -7,12 +7,13 @@ import ( "github.com/containerd/console" "github.com/docker/buildx/build" - controllerapi "github.com/docker/buildx/commands/controller/pb" - "github.com/docker/buildx/monitor" + cbuild "github.com/docker/buildx/controller/build" + "github.com/docker/buildx/controller/control" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/cli/cli/command" ) -func newLocalBuildxController(ctx context.Context, dockerCli command.Cli) monitor.BuildxController { +func NewLocalBuildxController(ctx context.Context, dockerCli command.Cli) control.BuildxController { return &localController{ dockerCli: dockerCli, ref: "local", @@ -52,7 +53,7 @@ func (b *localController) Invoke(ctx context.Context, ref string, cfg controller } func (b *localController) Build(ctx context.Context, options controllerapi.BuildOptions, in io.ReadCloser, w io.Writer, out console.File, progressMode string) (string, error) { - res, err := runBuildWithContext(ctx, b.dockerCli, options, in, progressMode, nil) + res, err := cbuild.RunBuild(ctx, b.dockerCli, options, in, progressMode, nil) if err != nil { return "", err } diff --git a/commands/controller/pb/controller.pb.go b/controller/pb/controller.pb.go similarity index 100% rename from commands/controller/pb/controller.pb.go rename to controller/pb/controller.pb.go diff --git a/commands/controller/pb/controller.proto b/controller/pb/controller.proto similarity index 100% rename from commands/controller/pb/controller.proto rename to controller/pb/controller.proto diff --git a/controller/pb/generate.go b/controller/pb/generate.go new file mode 100644 index 00000000..b997566e --- /dev/null +++ b/controller/pb/generate.go @@ -0,0 +1,3 @@ +package pb + +//go:generate protoc -I=. -I=../../vendor/ --gogo_out=plugins=grpc:. controller.proto diff --git a/commands/controller/client.go b/controller/remote/client.go similarity index 98% rename from commands/controller/client.go rename to controller/remote/client.go index 3a5b6dad..8fdfaa14 100644 --- a/commands/controller/client.go +++ b/controller/remote/client.go @@ -1,4 +1,4 @@ -package controller +package remote import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/containerd/console" "github.com/containerd/containerd/defaults" "github.com/containerd/containerd/pkg/dialer" - "github.com/docker/buildx/commands/controller/pb" + "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" "github.com/moby/buildkit/identity" diff --git a/commands/controllerremote.go b/controller/remote/controller.go similarity index 87% rename from commands/controllerremote.go rename to controller/remote/controller.go index a5dead05..c90b79f2 100644 --- a/commands/controllerremote.go +++ b/controller/remote/controller.go @@ -1,6 +1,6 @@ //go:build linux -package commands +package remote import ( "context" @@ -17,9 +17,9 @@ import ( "github.com/containerd/containerd/log" "github.com/docker/buildx/build" - "github.com/docker/buildx/commands/controller" - controllerapi "github.com/docker/buildx/commands/controller/pb" - "github.com/docker/buildx/monitor" + cbuild "github.com/docker/buildx/controller/build" + "github.com/docker/buildx/controller/control" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/version" "github.com/docker/cli/cli/command" @@ -45,8 +45,8 @@ type serverConfig struct { LogFile string `toml:"log_file"` } -func newRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts buildOptions) (monitor.BuildxController, error) { - rootDir := opts.root +func NewRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts control.ControlOptions) (control.BuildxController, error) { + rootDir := opts.Root if rootDir == "" { rootDir = rootDataDir(dockerCli) } @@ -56,10 +56,10 @@ func newRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts logrus.Info("no buildx server found; launching...") // start buildx server via subcommand launchFlags := []string{} - if opts.serverConfig != "" { - launchFlags = append(launchFlags, "--config", opts.serverConfig) + if opts.ServerConfig != "" { + launchFlags = append(launchFlags, "--config", opts.ServerConfig) } - logFile, err := getLogFilePath(dockerCli, opts.serverConfig) + logFile, err := getLogFilePath(dockerCli, opts.ServerConfig) if err != nil { return nil, err } @@ -76,13 +76,13 @@ func newRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts return &buildxController{c, serverRoot}, nil } -func addControllerCommands(cmd *cobra.Command, dockerCli command.Cli, rootOpts *rootOptions) { +func AddControllerCommands(cmd *cobra.Command, dockerCli command.Cli) { cmd.AddCommand( - serveCmd(dockerCli, rootOpts), + serveCmd(dockerCli), ) } -func serveCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { +func serveCmd(dockerCli command.Cli) *cobra.Command { var serverConfigPath string cmd := &cobra.Command{ Use: fmt.Sprintf("%s [OPTIONS]", serveCommandName), @@ -120,8 +120,8 @@ func serveCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { }() // prepare server - b := controller.New(func(ctx context.Context, options *controllerapi.BuildOptions, stdin io.Reader, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) { - return runBuildWithContext(ctx, dockerCli, *options, stdin, "quiet", statusChan) + b := NewServer(func(ctx context.Context, options *controllerapi.BuildOptions, stdin io.Reader, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) { + return cbuild.RunBuild(ctx, dockerCli, *options, stdin, "quiet", statusChan) }) defer b.Close() @@ -149,7 +149,6 @@ func serveCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { if err := rpc.Serve(l); err != nil { errCh <- fmt.Errorf("error on serving via socket %q: %w", addr, err) } - return }() var s os.Signal sigCh := make(chan os.Signal, 1) @@ -228,8 +227,8 @@ func rootDataDir(dockerCli command.Cli) string { return filepath.Join(confutil.ConfigDir(dockerCli), "controller") } -func newBuildxClientAndCheck(addr string, checkNum int, duration time.Duration) (*controller.Client, error) { - c, err := controller.NewClient(addr) +func newBuildxClientAndCheck(addr string, checkNum int, duration time.Duration) (*Client, error) { + c, err := NewClient(addr) if err != nil { return nil, err } @@ -261,7 +260,7 @@ func newBuildxClientAndCheck(addr string, checkNum int, duration time.Duration) } type buildxController struct { - *controller.Client + *Client serverRoot string } diff --git a/controller/remote/controller_nolinux.go b/controller/remote/controller_nolinux.go new file mode 100644 index 00000000..99d3a4be --- /dev/null +++ b/controller/remote/controller_nolinux.go @@ -0,0 +1,18 @@ +//go:build !linux + +package remote + +import ( + "context" + "fmt" + + "github.com/docker/buildx/controller/control" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +func NewRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts control.ControlOptions) (control.BuildxController, error) { + return nil, fmt.Errorf("remote buildx unsupported") +} + +func AddControllerCommands(cmd *cobra.Command, dockerCli command.Cli) {} diff --git a/commands/controller/io.go b/controller/remote/io.go similarity index 99% rename from commands/controller/io.go rename to controller/remote/io.go index 045073f1..63884b34 100644 --- a/commands/controller/io.go +++ b/controller/remote/io.go @@ -1,4 +1,4 @@ -package controller +package remote import ( "context" @@ -8,7 +8,7 @@ import ( "syscall" "time" - "github.com/docker/buildx/commands/controller/pb" + "github.com/docker/buildx/controller/pb" "github.com/moby/sys/signal" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" diff --git a/commands/controller/controller.go b/controller/remote/server.go similarity index 91% rename from commands/controller/controller.go rename to controller/remote/server.go index ed2bb366..77d4bdb3 100644 --- a/commands/controller/controller.go +++ b/controller/remote/server.go @@ -1,4 +1,4 @@ -package controller +package remote import ( "context" @@ -9,7 +9,7 @@ import ( "time" "github.com/docker/buildx/build" - "github.com/docker/buildx/commands/controller/pb" + "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/ioset" "github.com/docker/buildx/version" controlapi "github.com/moby/buildkit/api/services/control" @@ -20,13 +20,13 @@ import ( type BuildFunc func(ctx context.Context, options *pb.BuildOptions, stdin io.Reader, statusChan chan *client.SolveStatus) (res *build.ResultContext, err error) -func New(buildFunc BuildFunc) *Controller { - return &Controller{ +func NewServer(buildFunc BuildFunc) *Server { + return &Server{ buildFunc: buildFunc, } } -type Controller struct { +type Server struct { buildFunc BuildFunc session map[string]session sessionMu sync.Mutex @@ -40,7 +40,7 @@ type session struct { curBuildCancel func() } -func (m *Controller) Info(ctx context.Context, req *pb.InfoRequest) (res *pb.InfoResponse, err error) { +func (m *Server) Info(ctx context.Context, req *pb.InfoRequest) (res *pb.InfoResponse, err error) { return &pb.InfoResponse{ BuildxVersion: &pb.BuildxVersion{ Package: version.Package, @@ -50,7 +50,7 @@ func (m *Controller) Info(ctx context.Context, req *pb.InfoRequest) (res *pb.Inf }, nil } -func (m *Controller) List(ctx context.Context, req *pb.ListRequest) (res *pb.ListResponse, err error) { +func (m *Server) List(ctx context.Context, req *pb.ListRequest) (res *pb.ListResponse, err error) { keys := make(map[string]struct{}) m.sessionMu.Lock() @@ -68,7 +68,7 @@ func (m *Controller) List(ctx context.Context, req *pb.ListRequest) (res *pb.Lis }, nil } -func (m *Controller) Disconnect(ctx context.Context, req *pb.DisconnectRequest) (res *pb.DisconnectResponse, err error) { +func (m *Server) Disconnect(ctx context.Context, req *pb.DisconnectRequest) (res *pb.DisconnectResponse, err error) { key := req.Ref if key == "" { return nil, fmt.Errorf("disconnect: empty key") @@ -89,7 +89,7 @@ func (m *Controller) Disconnect(ctx context.Context, req *pb.DisconnectRequest) return &pb.DisconnectResponse{}, nil } -func (m *Controller) Close() error { +func (m *Server) Close() error { m.sessionMu.Lock() for k := range m.session { if s, ok := m.session[k]; ok { @@ -105,7 +105,7 @@ func (m *Controller) Close() error { return nil } -func (m *Controller) Build(ctx context.Context, req *pb.BuildRequest) (*pb.BuildResponse, error) { +func (m *Server) Build(ctx context.Context, req *pb.BuildRequest) (*pb.BuildResponse, error) { ref := req.Ref if ref == "" { return nil, fmt.Errorf("build: empty key") @@ -166,7 +166,7 @@ func (m *Controller) Build(ctx context.Context, req *pb.BuildRequest) (*pb.Build return &pb.BuildResponse{}, err } -func (m *Controller) Status(req *pb.StatusRequest, stream pb.Controller_StatusServer) error { +func (m *Server) Status(req *pb.StatusRequest, stream pb.Controller_StatusServer) error { ref := req.Ref if ref == "" { return fmt.Errorf("status: empty key") @@ -201,7 +201,7 @@ func (m *Controller) Status(req *pb.StatusRequest, stream pb.Controller_StatusSe return nil } -func (m *Controller) Input(stream pb.Controller_InputServer) (err error) { +func (m *Server) Input(stream pb.Controller_InputServer) (err error) { // Get the target ref from init message msg, err := stream.Recv() if err != nil { @@ -293,7 +293,7 @@ func (m *Controller) Input(stream pb.Controller_InputServer) (err error) { return eg.Wait() } -func (m *Controller) Invoke(srv pb.Controller_InvokeServer) error { +func (m *Server) Invoke(srv pb.Controller_InvokeServer) error { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() containerIn, containerOut := ioset.Pipe() diff --git a/monitor/monitor.go b/monitor/monitor.go index 41491fee..cc1b512a 100644 --- a/monitor/monitor.go +++ b/monitor/monitor.go @@ -10,7 +10,8 @@ import ( "text/tabwriter" "github.com/containerd/console" - controllerapi "github.com/docker/buildx/commands/controller/pb" + "github.com/docker/buildx/controller/control" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/ioset" "github.com/sirupsen/logrus" "golang.org/x/term" @@ -28,17 +29,8 @@ Available commads are: help shows this message. ` -type BuildxController interface { - Invoke(ctx context.Context, ref string, options controllerapi.ContainerConfig, ioIn io.ReadCloser, ioOut io.WriteCloser, ioErr io.WriteCloser) error - Build(ctx context.Context, options controllerapi.BuildOptions, in io.ReadCloser, w io.Writer, out console.File, progressMode string) (ref string, err error) - Kill(ctx context.Context) error - Close() error - List(ctx context.Context) (res []string, _ error) - Disconnect(ctx context.Context, ref string) error -} - // RunMonitor provides an interactive session for running and managing containers via specified IO. -func RunMonitor(ctx context.Context, curRef string, options controllerapi.BuildOptions, invokeConfig controllerapi.ContainerConfig, c BuildxController, progressMode string, stdin io.ReadCloser, stdout io.WriteCloser, stderr console.File) error { +func RunMonitor(ctx context.Context, curRef string, options controllerapi.BuildOptions, invokeConfig controllerapi.ContainerConfig, c control.BuildxController, progressMode string, stdin io.ReadCloser, stdout io.WriteCloser, stderr console.File) error { defer func() { if err := c.Disconnect(ctx, curRef); err != nil { logrus.Warnf("disconnect error: %v", err)