Skip to content

Commit 70db7cc

Browse files
authored
Merge pull request docker#758 from vdemeester/experimental-cli
Add support for experimental Cli configuration
2 parents ebb8927 + 84fe1a1 commit 70db7cc

7 files changed

Lines changed: 115 additions & 22 deletions

File tree

cli/command/cli.go

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,25 @@ type Cli interface {
4242
SetIn(in *InStream)
4343
ConfigFile() *configfile.ConfigFile
4444
ServerInfo() ServerInfo
45+
ClientInfo() ClientInfo
4546
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
4647
}
4748

4849
// DockerCli is an instance the docker command line client.
4950
// Instances of the client can be returned from NewDockerCli.
5051
type DockerCli struct {
51-
configFile *configfile.ConfigFile
52-
in *InStream
53-
out *OutStream
54-
err io.Writer
55-
client client.APIClient
56-
defaultVersion string
57-
server ServerInfo
52+
configFile *configfile.ConfigFile
53+
in *InStream
54+
out *OutStream
55+
err io.Writer
56+
client client.APIClient
57+
serverInfo ServerInfo
58+
clientInfo ClientInfo
5859
}
5960

6061
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
6162
func (cli *DockerCli) DefaultVersion() string {
62-
return cli.defaultVersion
63+
return cli.clientInfo.DefaultVersion
6364
}
6465

6566
// Client returns the APIClient
@@ -104,7 +105,12 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
104105
// ServerInfo returns the server version details for the host this client is
105106
// connected to
106107
func (cli *DockerCli) ServerInfo() ServerInfo {
107-
return cli.server
108+
return cli.serverInfo
109+
}
110+
111+
// ClientInfo returns the client details for the cli
112+
func (cli *DockerCli) ClientInfo() ClientInfo {
113+
return cli.clientInfo
108114
}
109115

110116
// Initialize the dockerCli runs initialization that must happen after command
@@ -125,25 +131,42 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
125131
if err != nil {
126132
return err
127133
}
134+
hasExperimental, err := isEnabled(cli.configFile.Experimental)
135+
if err != nil {
136+
return errors.Wrap(err, "Experimental field")
137+
}
138+
cli.clientInfo = ClientInfo{
139+
DefaultVersion: cli.client.ClientVersion(),
140+
HasExperimental: hasExperimental,
141+
}
128142
cli.initializeFromClient()
129143
return nil
130144
}
131145

132-
func (cli *DockerCli) initializeFromClient() {
133-
cli.defaultVersion = cli.client.ClientVersion()
146+
func isEnabled(value string) (bool, error) {
147+
switch value {
148+
case "enabled":
149+
return true, nil
150+
case "", "disabled":
151+
return false, nil
152+
default:
153+
return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
154+
}
155+
}
134156

157+
func (cli *DockerCli) initializeFromClient() {
135158
ping, err := cli.client.Ping(context.Background())
136159
if err != nil {
137160
// Default to true if we fail to connect to daemon
138-
cli.server = ServerInfo{HasExperimental: true}
161+
cli.serverInfo = ServerInfo{HasExperimental: true}
139162

140163
if ping.APIVersion != "" {
141164
cli.client.NegotiateAPIVersionPing(ping)
142165
}
143166
return
144167
}
145168

146-
cli.server = ServerInfo{
169+
cli.serverInfo = ServerInfo{
147170
HasExperimental: ping.Experimental,
148171
OSType: ping.OSType,
149172
}
@@ -176,6 +199,12 @@ type ServerInfo struct {
176199
OSType string
177200
}
178201

202+
// ClientInfo stores details about the supported features of the client
203+
type ClientInfo struct {
204+
HasExperimental bool
205+
DefaultVersion string
206+
}
207+
179208
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
180209
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
181210
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}

cli/command/cli_test.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package command
22

33
import (
4+
"crypto/x509"
45
"os"
56
"testing"
67

7-
"crypto/x509"
8-
8+
cliconfig "github.com/docker/cli/cli/config"
99
"github.com/docker/cli/cli/config/configfile"
1010
"github.com/docker/cli/cli/flags"
1111
"github.com/docker/cli/internal/test/testutil"
1212
"github.com/docker/docker/api"
1313
"github.com/docker/docker/api/types"
1414
"github.com/docker/docker/client"
15+
"github.com/gotestyourself/gotestyourself/fs"
1516
"github.com/pkg/errors"
1617
"github.com/stretchr/testify/assert"
1718
"github.com/stretchr/testify/require"
@@ -124,13 +125,51 @@ func TestInitializeFromClient(t *testing.T) {
124125

125126
cli := &DockerCli{client: apiclient}
126127
cli.initializeFromClient()
127-
assert.Equal(t, defaultVersion, cli.defaultVersion)
128-
assert.Equal(t, testcase.expectedServer, cli.server)
128+
assert.Equal(t, testcase.expectedServer, cli.serverInfo)
129129
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
130130
})
131131
}
132132
}
133133

134+
func TestExperimentalCLI(t *testing.T) {
135+
defaultVersion := "v1.55"
136+
137+
var testcases = []struct {
138+
doc string
139+
configfile string
140+
expectedExperimentalCLI bool
141+
}{
142+
{
143+
doc: "default",
144+
configfile: `{}`,
145+
expectedExperimentalCLI: false,
146+
},
147+
{
148+
doc: "experimental",
149+
configfile: `{
150+
"experimental": "enabled"
151+
}`,
152+
expectedExperimentalCLI: true,
153+
},
154+
}
155+
156+
for _, testcase := range testcases {
157+
t.Run(testcase.doc, func(t *testing.T) {
158+
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
159+
defer dir.Remove()
160+
apiclient := &fakeClient{
161+
version: defaultVersion,
162+
}
163+
164+
cli := &DockerCli{client: apiclient, err: os.Stderr}
165+
cliconfig.SetDir(dir.Path())
166+
err := cli.Initialize(flags.NewClientOptions())
167+
assert.NoError(t, err)
168+
assert.Equal(t, testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental)
169+
})
170+
}
171+
}
172+
134173
func TestGetClientWithPassword(t *testing.T) {
135174
expected := "password"
136175

cli/command/system/version.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
2323
Git commit: {{.GitCommit}}
2424
Built: {{.BuildTime}}
2525
OS/Arch: {{.Os}}/{{.Arch}}
26+
Experimental: {{.Experimental}}
2627
{{- end}}
2728
2829
{{- if .ServerOK}}{{with .Server}}
@@ -69,6 +70,7 @@ type clientVersion struct {
6970
Os string
7071
Arch string
7172
BuildTime string `json:",omitempty"`
73+
Experimental bool
7274
}
7375

7476
// ServerOK returns true when the client could connect to the docker server
@@ -133,6 +135,7 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
133135
BuildTime: cli.BuildTime,
134136
Os: runtime.GOOS,
135137
Arch: runtime.GOARCH,
138+
Experimental: dockerCli.ClientInfo().HasExperimental,
136139
},
137140
}
138141
vd.Client.Platform.Name = cli.PlatformName

cli/command/trust/cmd.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99
// NewTrustCommand returns a cobra command for `trust` subcommands
1010
func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
1111
cmd := &cobra.Command{
12-
Use: "trust",
13-
Short: "Manage trust on Docker images (experimental)",
14-
Args: cli.NoArgs,
15-
RunE: command.ShowHelp(dockerCli.Err()),
12+
Use: "trust",
13+
Short: "Manage trust on Docker images (experimental)",
14+
Args: cli.NoArgs,
15+
RunE: command.ShowHelp(dockerCli.Err()),
16+
Annotations: map[string]string{"experimentalCLI": ""},
1617
}
1718
cmd.AddCommand(
1819
newViewCommand(dockerCli),

cli/config/configfile/file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type ConfigFile struct {
4444
NodesFormat string `json:"nodesFormat,omitempty"`
4545
PruneFilters []string `json:"pruneFilters,omitempty"`
4646
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
47+
Experimental string `json:"experimental,omitempty"`
4748
}
4849

4950
// ProxyConfig contains proxy configuration settings

cmd/docker/docker.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,15 @@ func dockerPreRun(opts *cliflags.ClientOptions) {
193193

194194
type versionDetails interface {
195195
Client() client.APIClient
196+
ClientInfo() command.ClientInfo
196197
ServerInfo() command.ServerInfo
197198
}
198199

199200
func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
200201
clientVersion := details.Client().ClientVersion()
201202
osType := details.ServerInfo().OSType
202203
hasExperimental := details.ServerInfo().HasExperimental
204+
hasExperimentalCLI := details.ClientInfo().HasExperimental
203205

204206
cmd.Flags().VisitAll(func(f *pflag.Flag) {
205207
// hide experimental flags
@@ -208,6 +210,11 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
208210
f.Hidden = true
209211
}
210212
}
213+
if !hasExperimentalCLI {
214+
if _, ok := f.Annotations["experimentalCLI"]; ok {
215+
f.Hidden = true
216+
}
217+
}
211218

212219
// hide flags not supported by the server
213220
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
@@ -222,6 +229,11 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
222229
subcmd.Hidden = true
223230
}
224231
}
232+
if !hasExperimentalCLI {
233+
if _, ok := subcmd.Annotations["experimentalCLI"]; ok {
234+
subcmd.Hidden = true
235+
}
236+
}
225237

226238
// hide subcommands not supported by the server
227239
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
@@ -234,6 +246,7 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
234246
clientVersion := details.Client().ClientVersion()
235247
osType := details.ServerInfo().OSType
236248
hasExperimental := details.ServerInfo().HasExperimental
249+
hasExperimentalCLI := details.ClientInfo().HasExperimental
237250

238251
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
239252
for curr := cmd; curr != nil; curr = curr.Parent() {
@@ -243,6 +256,9 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
243256
if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
244257
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
245258
}
259+
if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
260+
return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath())
261+
}
246262
}
247263

248264
errs := []string{}
@@ -260,6 +276,9 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
260276
if _, ok := f.Annotations["experimental"]; ok && !hasExperimental {
261277
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name))
262278
}
279+
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
280+
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name))
281+
}
263282
}
264283
})
265284
if len(errs) > 0 {

e2e/internal/fixtures/fixtures.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ func SetupConfigFile(t *testing.T) fs.Dir {
3232
"https://notary-server:4443": {
3333
"auth": "ZWlhaXM6cGFzc3dvcmQK"
3434
}
35-
}
35+
},
36+
"experimental": "enabled"
3637
}
3738
`))
3839
return *dir

0 commit comments

Comments
 (0)