-
Notifications
You must be signed in to change notification settings - Fork 2.2k
cmd/docker: Add --cloud with configurable context resolution #7343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/docker/cli/cli" | ||
| pluginmanager "github.com/docker/cli/cli-plugins/manager" | ||
| "github.com/docker/cli/cli-plugins/metadata" | ||
| "github.com/docker/cli/cli/command" | ||
| "github.com/docker/cli/cli/config" | ||
| contextdocker "github.com/docker/cli/cli/context/docker" | ||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/pflag" | ||
| ) | ||
|
|
||
| // processCloud resolves the global --cloud[=NAME] option after configuration is | ||
| // loaded, but before telemetry and endpoint initialization. | ||
| // The option appears in help and completion only when the selected provider is | ||
| // installed, valid, and declares support for the resolver contract. | ||
| // The provider defaults to the offload plugin; features.cloud in the effective | ||
| // Docker config.json can override the plugin name, not its executable path. | ||
| // Explicit use also requires the provider's capability declaration. | ||
| // Discovery for visibility does not provision a context or connect to a daemon. | ||
| // | ||
| // # Provider contract | ||
| // | ||
| // The provider must include "Features": {"cloud-context-resolver": true} in its | ||
| // docker-cli-plugin-metadata response to declare support for this contract. | ||
| // Older plugins without this declaration leave the flag hidden and cannot be | ||
| // used for resolution. | ||
| // The CLI uses normal plugin discovery and invokes: | ||
| // | ||
| // docker-<provider> --config=<dir> <provider> __resolve-context -- <name> | ||
| // | ||
| // Bare --cloud and --cloud= pass default as the target name. | ||
| // The effective config directory is also passed as DOCKER_CONFIG. | ||
| // The provider must provision into that context store without changing the saved | ||
| // current context or recursively forwarding --cloud. | ||
| // It inherits the environment, receives no interactive stdin, and sends progress | ||
| // to stderr. | ||
| // Stdout must contain exactly one JSON object: | ||
| // | ||
| // {"DOCKER_CONTEXT":"provisioned-context"} | ||
| // | ||
| // The returned context must exist, must not be the virtual default context, and | ||
| // must have a Docker endpoint with a nonempty host. | ||
| // Resolver failures abort command execution without falling back to another | ||
| // context. | ||
| // Cancellation terminates the resolver process; provisioning subprocesses and | ||
| // resource cleanup remain the provider's responsibility. | ||
| // Downstream plugins receive --context=<resolved-name> instead of the global | ||
| // --cloud option. | ||
| // The provider implementation ships separately and must support this operation | ||
| // before enabling the integration. | ||
| func processCloud(ctx context.Context, dockerCli *command.DockerCli, cmd *cobra.Command, args, osArgs []string) ([]string, error) { | ||
| if !cmd.Flags().Changed("cloud") { | ||
| return osArgs, nil | ||
| } | ||
| if cmd.Flags().Changed("context") || cmd.Flags().Changed("host") { | ||
| return osArgs, errors.New("conflicting options: cannot specify --cloud together with --context or --host") | ||
| } | ||
|
|
||
| help, err := cloudHelpRequest(cmd, args) | ||
| if err != nil { | ||
| return osArgs, err | ||
| } | ||
|
|
||
| var contextName string | ||
| if !help { | ||
| name, _ := cmd.Flags().GetString("cloud") | ||
| if name == "" { | ||
| name = "default" | ||
| } | ||
|
|
||
| contextName, err = resolveCloudContext(ctx, dockerCli, cmd, name) | ||
| if err != nil { | ||
| return osArgs, fmt.Errorf("--cloud: %w", err) | ||
| } | ||
| if err := cmd.Flags().Set("context", contextName); err != nil { | ||
| return osArgs, err | ||
| } | ||
| } | ||
|
|
||
| return cloudPluginArgs(cmd, osArgs, contextName) | ||
| } | ||
|
|
||
| // cloudResolverFeature is the plugin metadata feature that declares support for | ||
| // the __resolve-context contract. | ||
| const cloudResolverFeature = "cloud-context-resolver" | ||
|
|
||
| func cloudProvider(dockerCli config.Provider, rootCmd *cobra.Command) (*pluginmanager.Plugin, error) { | ||
| provider := dockerCli.ConfigFile().Features["cloud"] | ||
| if provider == "" { | ||
| provider = "offload" | ||
| } | ||
|
|
||
| plugin, err := pluginmanager.GetPlugin(provider, dockerCli, rootCmd) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cloud resolver plugin %q unavailable: %w", provider, err) | ||
| } | ||
| if plugin.Err != nil { | ||
| return nil, fmt.Errorf("invalid cloud resolver plugin %q: %w", provider, plugin.Err) | ||
| } | ||
|
|
||
| if supported, _ := plugin.Features[cloudResolverFeature].(bool); !supported { | ||
| return nil, fmt.Errorf("plugin %q does not support cloud context resolution", provider) | ||
| } | ||
|
|
||
| return plugin, nil | ||
| } | ||
|
|
||
| // updateCloudFlagVisibility runs only for help and completion, avoiding plugin | ||
| // discovery on ordinary invocations that do not use --cloud. | ||
| func updateCloudFlagVisibility(dockerCli config.Provider, rootCmd *cobra.Command) { | ||
| flag := rootCmd.Flags().Lookup("cloud") | ||
| if flag == nil { | ||
| return | ||
| } | ||
| _, err := cloudProvider(dockerCli, rootCmd) | ||
| flag.Hidden = err != nil | ||
| } | ||
|
|
||
| func resolveCloudContext(ctx context.Context, dockerCli *command.DockerCli, rootCmd *cobra.Command, name string) (string, error) { | ||
| if err := ctx.Err(); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| plugin, err := cloudProvider(dockerCli, rootCmd) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| cmd := exec.CommandContext(ctx, plugin.Path, "--config="+config.Dir(), plugin.Name, "__resolve-context", "--", name) // #nosec G204 -- executable validated through CLI plugin discovery | ||
| cmd.Env = append(os.Environ(), config.EnvOverrideConfigDir+"="+config.Dir(), metadata.ReexecEnvvar+"="+os.Args[0]) | ||
| cmd.Stderr = dockerCli.Err() | ||
|
|
||
| out, err := cmd.Output() | ||
| if err != nil { | ||
| if ctx.Err() != nil { | ||
| return "", ctx.Err() | ||
| } | ||
| return "", fmt.Errorf("cloud resolver failed: %w", err) | ||
| } | ||
|
|
||
| var response struct { | ||
| DockerContext string `json:"DOCKER_CONTEXT"` | ||
| } | ||
| if err := json.Unmarshal(out, &response); err != nil { | ||
| return "", fmt.Errorf("invalid cloud resolver response: %w", err) | ||
| } | ||
| response.DockerContext = strings.TrimSpace(response.DockerContext) | ||
| if response.DockerContext == "" || response.DockerContext == command.DefaultContextName { | ||
| return "", errors.New("cloud resolver must return a non-default DOCKER_CONTEXT") | ||
| } | ||
|
|
||
| // Do not allow a missing context or endpoint to fall back to the local engine. | ||
| meta, err := dockerCli.ContextStore().GetMetadata(response.DockerContext) | ||
| if err != nil { | ||
| return "", fmt.Errorf("loading resolved context %q: %w", response.DockerContext, err) | ||
| } | ||
|
|
||
| endpoint, err := contextdocker.EndpointFromContext(meta) | ||
| if err != nil { | ||
| return "", fmt.Errorf("invalid resolved context %q: %w", response.DockerContext, err) | ||
| } | ||
| if endpoint.Host == "" { | ||
| return "", fmt.Errorf("resolved context %q has no Docker endpoint host", response.DockerContext) | ||
| } | ||
|
|
||
| return response.DockerContext, nil | ||
| } | ||
|
|
||
| // cloudHelpRequest avoids provisioning for help and shell completion. | ||
| // Parse known command flags rather than scanning argv: --help may be an option | ||
| // value or an argument to a container, not a request for Docker help. | ||
| // Unknown flags and missing flag values return a usage error, so a mistyped | ||
| // invocation fails without provisioning or falling back to the local engine. | ||
| // Flag values are validated only by the command's own parse, after resolution. | ||
| func cloudHelpRequest(rootCmd *cobra.Command, args []string) (bool, error) { | ||
| help, _ := rootCmd.PersistentFlags().GetBool("help") | ||
| version, _ := rootCmd.Flags().GetBool("version") | ||
| if help || version || len(args) == 0 { | ||
| return true, nil | ||
| } | ||
|
|
||
| switch args[0] { | ||
| case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: | ||
| return true, nil | ||
| } | ||
|
|
||
| cmd, remaining, err := rootCmd.Find(args) | ||
| if err != nil || cmd == rootCmd || pluginmanager.IsPluginCommand(cmd) { | ||
| // Plugin flags are opaque. Only recognize help immediately after the | ||
| // plugin name; deeper help is available through "docker help PLUGIN". | ||
| return len(args) > 1 && (args[1] == "--help" || args[1] == "-h" || args[1] == "--help=true"), nil | ||
|
Comment on lines
+199
to
+202
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah not sure theres a better way. We can't just scan the whole command line.. what about something like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's a good way around this due to the early context resolution that needs to happen before invoking the plugin. We could do a naive/full scan for
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
vvoland marked this conversation as resolved.
|
||
| } | ||
|
|
||
| cmd.InitDefaultHelpFlag() | ||
| flags := cmd.Flags() | ||
|
vvoland marked this conversation as resolved.
|
||
| flags.AddFlagSet(cmd.PersistentFlags()) | ||
| flags.AddFlagSet(cmd.InheritedFlags()) | ||
|
|
||
| err = flags.ParseAll(remaining, func(flag *pflag.Flag, value string) error { | ||
| if flag.Name == "help" { | ||
| var err error | ||
| if help, err = strconv.ParseBool(value); err != nil { | ||
| return fmt.Errorf("invalid argument %q for \"--help\" flag: %w", value, err) | ||
| } | ||
| } | ||
| return nil | ||
|
Comment on lines
+210
to
+217
|
||
| }) | ||
| if err != nil { | ||
| // Format the usage error directly. The root command's FlagErrorFunc | ||
| // first checks whether the daemon supports the command, which would | ||
| // connect to the engine selected before --cloud is resolved. | ||
| return false, cli.FlagErrorFunc(cmd, err) | ||
| } | ||
|
|
||
| return help, nil | ||
| } | ||
|
|
||
| // cloudPluginArgs only rewrites the global prefix, leaving subcommand arguments | ||
| // untouched. Parsing also distinguishes --cloud from another flag's value. | ||
| func cloudPluginArgs(cmd *cobra.Command, osArgs []string, contextName string) ([]string, error) { | ||
|
vvoland marked this conversation as resolved.
|
||
| flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError) | ||
| flags.SetInterspersed(false) | ||
| flags.AddFlagSet(cmd.Flags()) | ||
| flags.AddFlagSet(cmd.PersistentFlags()) | ||
|
|
||
| result := []string{osArgs[0]} | ||
| if contextName != "" { | ||
| result = append(result, "--context="+contextName) | ||
| } | ||
|
|
||
| if err := flags.ParseAll(osArgs[1:], func(flag *pflag.Flag, value string) error { | ||
| if flag.Name != "cloud" { | ||
| result = append(result, "--"+flag.Name+"="+value) | ||
|
vvoland marked this conversation as resolved.
|
||
| } | ||
| return nil | ||
| }); err != nil { | ||
| return osArgs, err | ||
| } | ||
|
|
||
| if flags.ArgsLenAtDash() >= 0 { | ||
| result = append(result, "--") | ||
| } | ||
|
|
||
| return append(result, flags.Args()...), nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.