mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-09 21:17:09 +08:00
controller: strongly type the controller api
Strongly typing the API allows us to perform all command line parsing fully on the client-side, where we have access to the client local directory and all the client environment variables, which may not be available on the remote server. Additionally, the controller api starts to look a lot like build.Options, so at some point in the future there may be an oppportunity to merge the two, which would allow both build and bake to execute through the controller, instead of needing to maintain multiple code paths. Signed-off-by: Justin Chadwell <me@jedevc.com>
This commit is contained in:
@ -19,6 +19,7 @@ import (
|
||||
"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/ioset"
|
||||
"github.com/docker/buildx/util/tracing"
|
||||
"github.com/docker/cli-docs-tool/annotation"
|
||||
@ -26,7 +27,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/buildkit/util/appcontext"
|
||||
"github.com/moby/buildkit/util/grpcerrors"
|
||||
"github.com/pkg/errors"
|
||||
@ -37,12 +37,93 @@ import (
|
||||
)
|
||||
|
||||
type buildOptions struct {
|
||||
progress string
|
||||
allow []string
|
||||
attests []string
|
||||
buildArgs []string
|
||||
cacheFrom []string
|
||||
cacheTo []string
|
||||
cgroupParent string
|
||||
contextPath string
|
||||
contexts []string
|
||||
dockerfileName string
|
||||
extraHosts []string
|
||||
imageIDFile string
|
||||
labels []string
|
||||
networkMode string
|
||||
noCacheFilter []string
|
||||
outputs []string
|
||||
platforms []string
|
||||
printFunc string
|
||||
quiet bool
|
||||
secrets []string
|
||||
shmSize dockeropts.MemBytes
|
||||
ssh []string
|
||||
tags []string
|
||||
target string
|
||||
ulimits *dockeropts.UlimitOpt
|
||||
|
||||
invoke string
|
||||
controllerapi.BuildOptions
|
||||
progress string
|
||||
|
||||
controllerapi.CommonOptions
|
||||
control.ControlOptions
|
||||
}
|
||||
|
||||
func (o *buildOptions) toControllerOptions() (controllerapi.BuildOptions, error) {
|
||||
var err error
|
||||
opts := controllerapi.BuildOptions{
|
||||
Allow: o.allow,
|
||||
Attests: o.attests,
|
||||
BuildArgs: listToMap(o.buildArgs, true),
|
||||
CgroupParent: o.cgroupParent,
|
||||
ContextPath: o.contextPath,
|
||||
DockerfileName: o.dockerfileName,
|
||||
ExtraHosts: o.extraHosts,
|
||||
ImageIDFile: o.imageIDFile,
|
||||
Labels: listToMap(o.labels, false),
|
||||
NetworkMode: o.networkMode,
|
||||
NoCacheFilter: o.noCacheFilter,
|
||||
Platforms: o.platforms,
|
||||
PrintFunc: o.printFunc,
|
||||
Quiet: o.quiet,
|
||||
ShmSize: int64(o.shmSize),
|
||||
Tags: o.tags,
|
||||
Target: o.target,
|
||||
Ulimits: dockerUlimitToControllerUlimit(o.ulimits),
|
||||
Opts: &o.CommonOptions,
|
||||
}
|
||||
|
||||
opts.NamedContexts, err = buildflags.ParseContextNames(o.contexts)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
|
||||
opts.Exports, err = buildflags.ParseExports(o.outputs)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
|
||||
opts.CacheFrom, err = buildflags.ParseCacheEntry(o.cacheFrom)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
opts.CacheTo, err = buildflags.ParseCacheEntry(o.cacheTo)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
|
||||
opts.Secrets, err = buildflags.ParseSecretSpecs(o.secrets)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
opts.SSH, err = buildflags.ParseSSHSpecs(o.ssh)
|
||||
if err != nil {
|
||||
return controllerapi.BuildOptions{}, err
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func runBuild(dockerCli command.Cli, in buildOptions) error {
|
||||
ctx := appcontext.Context()
|
||||
|
||||
@ -54,12 +135,16 @@ func runBuild(dockerCli command.Cli, in buildOptions) error {
|
||||
end(err)
|
||||
}()
|
||||
|
||||
_, err = cbuild.RunBuild(ctx, dockerCli, in.BuildOptions, os.Stdin, in.progress, nil)
|
||||
opts, err := in.toControllerOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cbuild.RunBuild(ctx, dockerCli, opts, os.Stdin, in.progress, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
options := newBuildOptions()
|
||||
options := buildOptions{}
|
||||
cFlags := &commonFlags{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -68,16 +153,16 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
Short: "Start a build",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.ContextPath = args[0]
|
||||
options.Opts.Builder = rootOpts.builder
|
||||
options.Opts.MetadataFile = cFlags.metadataFile
|
||||
options.Opts.NoCache = false
|
||||
options.contextPath = args[0]
|
||||
options.Builder = rootOpts.builder
|
||||
options.MetadataFile = cFlags.metadataFile
|
||||
options.NoCache = false
|
||||
if cFlags.noCache != nil {
|
||||
options.Opts.NoCache = *cFlags.noCache
|
||||
options.NoCache = *cFlags.noCache
|
||||
}
|
||||
options.Opts.Pull = false
|
||||
options.Pull = false
|
||||
if cFlags.pull != nil {
|
||||
options.Opts.Pull = *cFlags.pull
|
||||
options.Pull = *cFlags.pull
|
||||
}
|
||||
options.progress = cFlags.progress
|
||||
cmd.Flags().VisitAll(checkWarnedFlags)
|
||||
@ -95,64 +180,65 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringSliceVar(&options.ExtraHosts, "add-host", []string{}, `Add a custom host-to-IP mapping (format: "host:ip")`)
|
||||
flags.StringSliceVar(&options.extraHosts, "add-host", []string{}, `Add a custom host-to-IP mapping (format: "host:ip")`)
|
||||
flags.SetAnnotation("add-host", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#add-host"})
|
||||
|
||||
flags.StringSliceVar(&options.Allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure")`)
|
||||
flags.StringSliceVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure")`)
|
||||
|
||||
flags.StringArrayVar(&options.BuildArgs, "build-arg", []string{}, "Set build-time variables")
|
||||
flags.StringArrayVar(&options.buildArgs, "build-arg", []string{}, "Set build-time variables")
|
||||
|
||||
flags.StringArrayVar(&options.CacheFrom, "cache-from", []string{}, `External cache sources (e.g., "user/app:cache", "type=local,src=path/to/dir")`)
|
||||
flags.StringArrayVar(&options.cacheFrom, "cache-from", []string{}, `External cache sources (e.g., "user/app:cache", "type=local,src=path/to/dir")`)
|
||||
|
||||
flags.StringArrayVar(&options.CacheTo, "cache-to", []string{}, `Cache export destinations (e.g., "user/app:cache", "type=local,dest=path/to/dir")`)
|
||||
flags.StringArrayVar(&options.cacheTo, "cache-to", []string{}, `Cache export destinations (e.g., "user/app:cache", "type=local,dest=path/to/dir")`)
|
||||
|
||||
flags.StringVar(&options.CgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
|
||||
flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
|
||||
flags.SetAnnotation("cgroup-parent", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#cgroup-parent"})
|
||||
|
||||
flags.StringArrayVar(&options.Contexts, "build-context", []string{}, "Additional build contexts (e.g., name=path)")
|
||||
flags.StringArrayVar(&options.contexts, "build-context", []string{}, "Additional build contexts (e.g., name=path)")
|
||||
|
||||
flags.StringVarP(&options.DockerfileName, "file", "f", "", `Name of the Dockerfile (default: "PATH/Dockerfile")`)
|
||||
flags.StringVarP(&options.dockerfileName, "file", "f", "", `Name of the Dockerfile (default: "PATH/Dockerfile")`)
|
||||
flags.SetAnnotation("file", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#file"})
|
||||
|
||||
flags.StringVar(&options.ImageIDFile, "iidfile", "", "Write the image ID to the file")
|
||||
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
|
||||
|
||||
flags.StringArrayVar(&options.Labels, "label", []string{}, "Set metadata for an image")
|
||||
flags.StringArrayVar(&options.labels, "label", []string{}, "Set metadata for an image")
|
||||
|
||||
flags.BoolVar(&options.Opts.ExportLoad, "load", false, `Shorthand for "--output=type=docker"`)
|
||||
flags.BoolVar(&options.ExportLoad, "load", false, `Shorthand for "--output=type=docker"`)
|
||||
|
||||
flags.StringVar(&options.NetworkMode, "network", "default", `Set the networking mode for the "RUN" instructions during build`)
|
||||
flags.StringVar(&options.networkMode, "network", "default", `Set the networking mode for the "RUN" instructions during build`)
|
||||
|
||||
flags.StringArrayVar(&options.NoCacheFilter, "no-cache-filter", []string{}, "Do not cache specified stages")
|
||||
flags.StringArrayVar(&options.noCacheFilter, "no-cache-filter", []string{}, "Do not cache specified stages")
|
||||
|
||||
flags.StringArrayVarP(&options.Outputs, "output", "o", []string{}, `Output destination (format: "type=local,dest=path")`)
|
||||
flags.StringArrayVarP(&options.outputs, "output", "o", []string{}, `Output destination (format: "type=local,dest=path")`)
|
||||
|
||||
flags.StringArrayVar(&options.Platforms, "platform", platformsDefault, "Set target platform for build")
|
||||
flags.StringArrayVar(&options.platforms, "platform", platformsDefault, "Set target platform for build")
|
||||
|
||||
if isExperimental() {
|
||||
flags.StringVar(&options.PrintFunc, "print", "", "Print result of information request (e.g., outline, targets) [experimental]")
|
||||
flags.StringVar(&options.printFunc, "print", "", "Print result of information request (e.g., outline, targets) [experimental]")
|
||||
}
|
||||
|
||||
flags.BoolVar(&options.Opts.ExportPush, "push", false, `Shorthand for "--output=type=registry"`)
|
||||
flags.BoolVar(&options.ExportPush, "push", false, `Shorthand for "--output=type=registry"`)
|
||||
|
||||
flags.BoolVarP(&options.Quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
|
||||
|
||||
flags.StringArrayVar(&options.Secrets, "secret", []string{}, `Secret to expose to the build (format: "id=mysecret[,src=/local/secret]")`)
|
||||
flags.StringArrayVar(&options.secrets, "secret", []string{}, `Secret to expose to the build (format: "id=mysecret[,src=/local/secret]")`)
|
||||
|
||||
flags.Var(newShmSize(&options), "shm-size", `Size of "/dev/shm"`)
|
||||
flags.Var(&options.shmSize, "shm-size", `Size of "/dev/shm"`)
|
||||
|
||||
flags.StringArrayVar(&options.SSH, "ssh", []string{}, `SSH agent socket or keys to expose to the build (format: "default|<id>[=<socket>|<key>[,<key>]]")`)
|
||||
flags.StringArrayVar(&options.ssh, "ssh", []string{}, `SSH agent socket or keys to expose to the build (format: "default|<id>[=<socket>|<key>[,<key>]]")`)
|
||||
|
||||
flags.StringArrayVarP(&options.Tags, "tag", "t", []string{}, `Name and optionally a tag (format: "name:tag")`)
|
||||
flags.StringArrayVarP(&options.tags, "tag", "t", []string{}, `Name and optionally a tag (format: "name:tag")`)
|
||||
flags.SetAnnotation("tag", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#tag"})
|
||||
|
||||
flags.StringVar(&options.Target, "target", "", "Set the target build stage to build")
|
||||
flags.StringVar(&options.target, "target", "", "Set the target build stage to build")
|
||||
flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/engine/reference/commandline/build/#target"})
|
||||
|
||||
flags.Var(newUlimits(&options), "ulimit", "Ulimit options")
|
||||
options.ulimits = dockeropts.NewUlimitOpt(nil)
|
||||
flags.Var(options.ulimits, "ulimit", "Ulimit options")
|
||||
|
||||
flags.StringArrayVar(&options.Attests, "attest", []string{}, `Attestation parameters (format: "type=sbom,generator=image")`)
|
||||
flags.StringVar(&options.Opts.SBOM, "sbom", "", `Shorthand for "--attest=type=sbom"`)
|
||||
flags.StringVar(&options.Opts.Provenance, "provenance", "", `Shortand for "--attest=type=provenance"`)
|
||||
flags.StringArrayVar(&options.attests, "attest", []string{}, `Attestation parameters (format: "type=sbom,generator=image")`)
|
||||
flags.StringVar(&options.SBOM, "sbom", "", `Shorthand for "--attest=type=sbom"`)
|
||||
flags.StringVar(&options.Provenance, "provenance", "", `Shortand for "--attest=type=provenance"`)
|
||||
|
||||
if isExperimental() {
|
||||
flags.StringVar(&options.invoke, "invoke", "", "Invoke a command after the build [experimental]")
|
||||
@ -317,12 +403,12 @@ func updateLastActivity(dockerCli command.Cli, ng *store.NodeGroup) error {
|
||||
func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
ctx := context.TODO()
|
||||
|
||||
if options.Quiet && options.progress != "auto" && options.progress != "quiet" {
|
||||
if options.quiet && options.progress != "auto" && options.progress != "quiet" {
|
||||
return errors.Errorf("progress=%s and quiet cannot be used together", options.progress)
|
||||
} else if options.Quiet {
|
||||
} else if options.quiet {
|
||||
options.progress = "quiet"
|
||||
}
|
||||
if options.invoke != "" && (options.DockerfileName == "-" || options.ContextPath == "-") {
|
||||
if options.invoke != "" && (options.dockerfileName == "-" || options.contextPath == "-") {
|
||||
// stdin must be usable for monitor
|
||||
return errors.Errorf("Dockerfile or context from stdin is not supported with invoke")
|
||||
}
|
||||
@ -355,7 +441,11 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
|
||||
f.SetReader(os.Stdin)
|
||||
|
||||
// Start build
|
||||
ref, err := c.Build(ctx, options.BuildOptions, pr, os.Stdout, os.Stderr, options.progress)
|
||||
opts, err := options.toControllerOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref, err := c.Build(ctx, opts, pr, os.Stdout, os.Stderr, options.progress)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to build") // TODO: allow invoke even on error
|
||||
}
|
||||
@ -380,7 +470,7 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
|
||||
}
|
||||
return errors.Errorf("failed to configure terminal: %v", err)
|
||||
}
|
||||
err = monitor.RunMonitor(ctx, ref, options.BuildOptions, invokeConfig, c, options.progress, pr2, os.Stdout, os.Stderr)
|
||||
err = monitor.RunMonitor(ctx, ref, opts, invokeConfig, c, options.progress, pr2, os.Stdout, os.Stderr)
|
||||
con.Reset()
|
||||
if err := pw2.Close(); err != nil {
|
||||
logrus.Debug("failed to close monitor stdin pipe reader")
|
||||
@ -445,75 +535,37 @@ func parseInvokeConfig(invoke string) (cfg controllerapi.ContainerConfig, err er
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func newBuildOptions() buildOptions {
|
||||
return buildOptions{
|
||||
BuildOptions: controllerapi.BuildOptions{
|
||||
Opts: &controllerapi.CommonOptions{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newUlimits(opt *buildOptions) *ulimits {
|
||||
ul := make(map[string]*units.Ulimit)
|
||||
return &ulimits{opt: opt, org: dockeropts.NewUlimitOpt(&ul)}
|
||||
}
|
||||
|
||||
type ulimits struct {
|
||||
opt *buildOptions
|
||||
org *dockeropts.UlimitOpt
|
||||
}
|
||||
|
||||
func (u *ulimits) sync() {
|
||||
du := &controllerapi.UlimitOpt{
|
||||
Values: make(map[string]*controllerapi.Ulimit),
|
||||
}
|
||||
for _, l := range u.org.GetList() {
|
||||
du.Values[l.Name] = &controllerapi.Ulimit{
|
||||
Name: l.Name,
|
||||
Hard: l.Hard,
|
||||
Soft: l.Soft,
|
||||
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]
|
||||
}
|
||||
}
|
||||
u.opt.Ulimits = du
|
||||
return result
|
||||
}
|
||||
|
||||
func (u *ulimits) String() string {
|
||||
return u.org.String()
|
||||
}
|
||||
|
||||
func (u *ulimits) Set(v string) error {
|
||||
err := u.org.Set(v)
|
||||
u.sync()
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *ulimits) Type() string {
|
||||
return u.org.Type()
|
||||
}
|
||||
|
||||
func newShmSize(opt *buildOptions) *shmSize {
|
||||
return &shmSize{opt: opt}
|
||||
}
|
||||
|
||||
type shmSize struct {
|
||||
opt *buildOptions
|
||||
org dockeropts.MemBytes
|
||||
}
|
||||
|
||||
func (s *shmSize) sync() {
|
||||
s.opt.ShmSize = s.org.Value()
|
||||
}
|
||||
|
||||
func (s *shmSize) String() string {
|
||||
return s.org.String()
|
||||
}
|
||||
|
||||
func (s *shmSize) Set(v string) error {
|
||||
err := s.org.Set(v)
|
||||
s.sync()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *shmSize) Type() string {
|
||||
return s.org.Type()
|
||||
func dockerUlimitToControllerUlimit(u *dockeropts.UlimitOpt) *controllerapi.UlimitOpt {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
values := make(map[string]*controllerapi.Ulimit)
|
||||
for _, u := range u.GetList() {
|
||||
values[u.Name] = &controllerapi.Ulimit{
|
||||
Name: u.Name,
|
||||
Hard: u.Hard,
|
||||
Soft: u.Soft,
|
||||
}
|
||||
}
|
||||
return &controllerapi.UlimitOpt{Values: values}
|
||||
}
|
||||
|
Reference in New Issue
Block a user