mirror of
https://github.com/docker/cli.git
synced 2026-11-06 13:10:41 -04:00
Resolve it through the offload plugin by default, with a provider override in features.cloud in the Docker config. Require the selected plugin to declare the cloud-context-resolver feature in its metadata before showing the flag in help and completion or invoking its __resolve-context operation. Resolve through WithContextResolver, after the config file and context store are loaded but before client initialization, without wrapping or re-executing Docker. Forward --context to downstream plugins so they need not support --cloud or repeat provisioning. Skip provisioning for help, version, and completion requests. Report unknown flags and missing flag values before provisioning, so a mistyped invocation fails without creating cloud resources or contacting the engine selected before resolution. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
257 lines
9.0 KiB
Go
257 lines
9.0 KiB
Go
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
|
|
}
|
|
|
|
cmd.InitDefaultHelpFlag()
|
|
flags := cmd.Flags()
|
|
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
|
|
})
|
|
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) {
|
|
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)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return osArgs, err
|
|
}
|
|
|
|
if flags.ArgsLenAtDash() >= 0 {
|
|
result = append(result, "--")
|
|
}
|
|
|
|
return append(result, flags.Args()...), nil
|
|
}
|