mirror of https://github.com/docker/cli.git
Add support for experimental Cli configuration
Allow to mark some commands and flags experimental on cli (i.e. not depending to the state of the daemon). This will allow more flexibility on experimentation with the cli. Marking `docker trust` as cli experimental as it is documented so. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
2b8eb23b8c
commit
84fe1a1b5b
|
@ -42,6 +42,7 @@ type Cli interface {
|
||||||
SetIn(in *InStream)
|
SetIn(in *InStream)
|
||||||
ConfigFile() *configfile.ConfigFile
|
ConfigFile() *configfile.ConfigFile
|
||||||
ServerInfo() ServerInfo
|
ServerInfo() ServerInfo
|
||||||
|
ClientInfo() ClientInfo
|
||||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,13 +54,13 @@ type DockerCli struct {
|
||||||
out *OutStream
|
out *OutStream
|
||||||
err io.Writer
|
err io.Writer
|
||||||
client client.APIClient
|
client client.APIClient
|
||||||
defaultVersion string
|
serverInfo ServerInfo
|
||||||
server ServerInfo
|
clientInfo ClientInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||||
func (cli *DockerCli) DefaultVersion() string {
|
func (cli *DockerCli) DefaultVersion() string {
|
||||||
return cli.defaultVersion
|
return cli.clientInfo.DefaultVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client returns the APIClient
|
// Client returns the APIClient
|
||||||
|
@ -104,7 +105,12 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
||||||
// ServerInfo returns the server version details for the host this client is
|
// ServerInfo returns the server version details for the host this client is
|
||||||
// connected to
|
// connected to
|
||||||
func (cli *DockerCli) ServerInfo() ServerInfo {
|
func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||||
return cli.server
|
return cli.serverInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientInfo returns the client details for the cli
|
||||||
|
func (cli *DockerCli) ClientInfo() ClientInfo {
|
||||||
|
return cli.clientInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the dockerCli runs initialization that must happen after command
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
|
@ -125,17 +131,34 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
hasExperimental, err := isEnabled(cli.configFile.Experimental)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Experimental field")
|
||||||
|
}
|
||||||
|
cli.clientInfo = ClientInfo{
|
||||||
|
DefaultVersion: cli.client.ClientVersion(),
|
||||||
|
HasExperimental: hasExperimental,
|
||||||
|
}
|
||||||
cli.initializeFromClient()
|
cli.initializeFromClient()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *DockerCli) initializeFromClient() {
|
func isEnabled(value string) (bool, error) {
|
||||||
cli.defaultVersion = cli.client.ClientVersion()
|
switch value {
|
||||||
|
case "enabled":
|
||||||
|
return true, nil
|
||||||
|
case "", "disabled":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *DockerCli) initializeFromClient() {
|
||||||
ping, err := cli.client.Ping(context.Background())
|
ping, err := cli.client.Ping(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Default to true if we fail to connect to daemon
|
// Default to true if we fail to connect to daemon
|
||||||
cli.server = ServerInfo{HasExperimental: true}
|
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||||
|
|
||||||
if ping.APIVersion != "" {
|
if ping.APIVersion != "" {
|
||||||
cli.client.NegotiateAPIVersionPing(ping)
|
cli.client.NegotiateAPIVersionPing(ping)
|
||||||
|
@ -143,7 +166,7 @@ func (cli *DockerCli) initializeFromClient() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.server = ServerInfo{
|
cli.serverInfo = ServerInfo{
|
||||||
HasExperimental: ping.Experimental,
|
HasExperimental: ping.Experimental,
|
||||||
OSType: ping.OSType,
|
OSType: ping.OSType,
|
||||||
}
|
}
|
||||||
|
@ -176,6 +199,12 @@ type ServerInfo struct {
|
||||||
OSType string
|
OSType string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientInfo stores details about the supported features of the client
|
||||||
|
type ClientInfo struct {
|
||||||
|
HasExperimental bool
|
||||||
|
DefaultVersion string
|
||||||
|
}
|
||||||
|
|
||||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
|
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
|
||||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
|
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crypto/x509"
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/cli/flags"
|
"github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/cli/internal/test/testutil"
|
"github.com/docker/cli/internal/test/testutil"
|
||||||
"github.com/docker/docker/api"
|
"github.com/docker/docker/api"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/gotestyourself/gotestyourself/fs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -124,13 +125,51 @@ func TestInitializeFromClient(t *testing.T) {
|
||||||
|
|
||||||
cli := &DockerCli{client: apiclient}
|
cli := &DockerCli{client: apiclient}
|
||||||
cli.initializeFromClient()
|
cli.initializeFromClient()
|
||||||
assert.Equal(t, defaultVersion, cli.defaultVersion)
|
assert.Equal(t, testcase.expectedServer, cli.serverInfo)
|
||||||
assert.Equal(t, testcase.expectedServer, cli.server)
|
|
||||||
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
|
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExperimentalCLI(t *testing.T) {
|
||||||
|
defaultVersion := "v1.55"
|
||||||
|
|
||||||
|
var testcases = []struct {
|
||||||
|
doc string
|
||||||
|
configfile string
|
||||||
|
expectedExperimentalCLI bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "default",
|
||||||
|
configfile: `{}`,
|
||||||
|
expectedExperimentalCLI: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "experimental",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
expectedExperimentalCLI: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
t.Run(testcase.doc, func(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
||||||
|
defer dir.Remove()
|
||||||
|
apiclient := &fakeClient{
|
||||||
|
version: defaultVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||||
|
cliconfig.SetDir(dir.Path())
|
||||||
|
err := cli.Initialize(flags.NewClientOptions())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetClientWithPassword(t *testing.T) {
|
func TestGetClientWithPassword(t *testing.T) {
|
||||||
expected := "password"
|
expected := "password"
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
|
||||||
Git commit: {{.GitCommit}}
|
Git commit: {{.GitCommit}}
|
||||||
Built: {{.BuildTime}}
|
Built: {{.BuildTime}}
|
||||||
OS/Arch: {{.Os}}/{{.Arch}}
|
OS/Arch: {{.Os}}/{{.Arch}}
|
||||||
|
Experimental: {{.Experimental}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- if .ServerOK}}{{with .Server}}
|
{{- if .ServerOK}}{{with .Server}}
|
||||||
|
@ -69,6 +70,7 @@ type clientVersion struct {
|
||||||
Os string
|
Os string
|
||||||
Arch string
|
Arch string
|
||||||
BuildTime string `json:",omitempty"`
|
BuildTime string `json:",omitempty"`
|
||||||
|
Experimental bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerOK returns true when the client could connect to the docker server
|
// ServerOK returns true when the client could connect to the docker server
|
||||||
|
@ -133,6 +135,7 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||||
BuildTime: cli.BuildTime,
|
BuildTime: cli.BuildTime,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
|
Experimental: dockerCli.ClientInfo().HasExperimental,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
vd.Client.Platform.Name = cli.PlatformName
|
vd.Client.Platform.Name = cli.PlatformName
|
||||||
|
|
|
@ -13,6 +13,7 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "Manage trust on Docker images (experimental)",
|
Short: "Manage trust on Docker images (experimental)",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
|
Annotations: map[string]string{"experimentalCLI": ""},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newViewCommand(dockerCli),
|
newViewCommand(dockerCli),
|
||||||
|
|
|
@ -44,6 +44,7 @@ type ConfigFile struct {
|
||||||
NodesFormat string `json:"nodesFormat,omitempty"`
|
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||||
PruneFilters []string `json:"pruneFilters,omitempty"`
|
PruneFilters []string `json:"pruneFilters,omitempty"`
|
||||||
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
||||||
|
Experimental string `json:"experimental,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig contains proxy configuration settings
|
// ProxyConfig contains proxy configuration settings
|
||||||
|
|
|
@ -193,6 +193,7 @@ func dockerPreRun(opts *cliflags.ClientOptions) {
|
||||||
|
|
||||||
type versionDetails interface {
|
type versionDetails interface {
|
||||||
Client() client.APIClient
|
Client() client.APIClient
|
||||||
|
ClientInfo() command.ClientInfo
|
||||||
ServerInfo() command.ServerInfo
|
ServerInfo() command.ServerInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +201,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
clientVersion := details.Client().ClientVersion()
|
clientVersion := details.Client().ClientVersion()
|
||||||
osType := details.ServerInfo().OSType
|
osType := details.ServerInfo().OSType
|
||||||
hasExperimental := details.ServerInfo().HasExperimental
|
hasExperimental := details.ServerInfo().HasExperimental
|
||||||
|
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
||||||
|
|
||||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
// hide experimental flags
|
// hide experimental flags
|
||||||
|
@ -208,6 +210,11 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
f.Hidden = true
|
f.Hidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !hasExperimentalCLI {
|
||||||
|
if _, ok := f.Annotations["experimentalCLI"]; ok {
|
||||||
|
f.Hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// hide flags not supported by the server
|
// hide flags not supported by the server
|
||||||
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
|
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
|
||||||
|
@ -222,6 +229,11 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
subcmd.Hidden = true
|
subcmd.Hidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !hasExperimentalCLI {
|
||||||
|
if _, ok := subcmd.Annotations["experimentalCLI"]; ok {
|
||||||
|
subcmd.Hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// hide subcommands not supported by the server
|
// hide subcommands not supported by the server
|
||||||
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
|
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
|
||||||
|
@ -234,6 +246,7 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
clientVersion := details.Client().ClientVersion()
|
clientVersion := details.Client().ClientVersion()
|
||||||
osType := details.ServerInfo().OSType
|
osType := details.ServerInfo().OSType
|
||||||
hasExperimental := details.ServerInfo().HasExperimental
|
hasExperimental := details.ServerInfo().HasExperimental
|
||||||
|
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
||||||
|
|
||||||
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
||||||
for curr := cmd; curr != nil; curr = curr.Parent() {
|
for curr := cmd; curr != nil; curr = curr.Parent() {
|
||||||
|
@ -243,6 +256,9 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
|
if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
|
||||||
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
|
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
|
||||||
}
|
}
|
||||||
|
if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
||||||
|
return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := []string{}
|
errs := []string{}
|
||||||
|
@ -260,6 +276,9 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
if _, ok := f.Annotations["experimental"]; ok && !hasExperimental {
|
if _, ok := f.Annotations["experimental"]; ok && !hasExperimental {
|
||||||
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name))
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name))
|
||||||
}
|
}
|
||||||
|
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
||||||
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
|
|
@ -32,7 +32,8 @@ func SetupConfigFile(t *testing.T) fs.Dir {
|
||||||
"https://notary-server:4443": {
|
"https://notary-server:4443": {
|
||||||
"auth": "ZWlhaXM6cGFzc3dvcmQK"
|
"auth": "ZWlhaXM6cGFzc3dvcmQK"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"experimental": "enabled"
|
||||||
}
|
}
|
||||||
`))
|
`))
|
||||||
return *dir
|
return *dir
|
||||||
|
|
Loading…
Reference in New Issue