Skip to content

Commit deae842

Browse files
committed
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>
1 parent 2b8eb23 commit deae842

File tree

7 files changed

+115
-22
lines changed

7 files changed

+115
-22
lines changed

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: "experimentalCLI",
149+
configfile: `{
150+
"experimentalCLI": "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)