Always enable experimental features

The CLI disabled experimental features by default, requiring users
to set a configuration option to enable them.

Disabling experimental features was a request from Enterprise users
that did not want experimental features to be accessible.

We are changing this policy, and now enable experimental features
by default. Experimental features may still change and/or removed,
and will be highlighted in the documentation and "usage" output.

For example, the `docker manifest inspect --help` output now shows:

    EXPERIMENTAL:
      docker manifest inspect is an experimental feature.

      Experimental features provide early access to product functionality. These features
      may change between releases without warning or can be removed entirely from a future
      release. Learn more about experimental features: https://docs.docker.com/go/experimental/

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2020-10-02 14:19:34 +02:00
parent d9c36c2878
commit 977d3ae046
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
14 changed files with 48 additions and 126 deletions

View File

@ -101,6 +101,5 @@ func main() {
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: "testing",
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
})
}

View File

@ -15,7 +15,6 @@ type fakeCandidate struct {
path string
exec bool
meta string
allowExperimental bool
}
func (c *fakeCandidate) Path() string {
@ -30,7 +29,7 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
}
func TestValidateCandidate(t *testing.T) {
var (
const (
goodPluginName = NamePrefix + "goodplugin"
builtinName = NamePrefix + "builtin"
@ -70,14 +69,12 @@ func TestValidateCandidate(t *testing.T) {
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
{name: "experimental required", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}, invalid: "requires experimental CLI"},
// This one should work
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
{name: "valid + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`, allowExperimental: true}},
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental, allowExperimental: true}},
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
} {
t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
p, err := newPlugin(tc.c, fakeroot)
if tc.err != "" {
assert.ErrorContains(t, err, tc.err)
} else if tc.invalid != "" {

View File

@ -1,7 +1,6 @@
package manager
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
@ -30,16 +29,6 @@ func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
}
type errPluginRequireExperimental string
// Note: errPluginRequireExperimental implements notFound so that the plugin
// is skipped when listing the plugins.
func (e errPluginRequireExperimental) NotFound() {}
func (e errPluginRequireExperimental) Error() string {
return fmt.Sprintf("plugin candidate %q: requires experimental CLI", string(e))
}
type notFound interface{ NotFound() }
// IsNotFound is true if the given error is due to a plugin not being found.
@ -133,7 +122,7 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
continue
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
p, err := newPlugin(c, rootcmd)
if err != nil {
return nil, err
}
@ -181,19 +170,12 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
}
c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
plugin, err := newPlugin(c, rootcmd)
if err != nil {
return nil, err
}
if plugin.Err != nil {
// TODO: why are we not returning plugin.Err?
err := plugin.Err.(*pluginError).Cause()
// if an experimental plugin was invoked directly while experimental mode is off
// provide a more useful error message than "not found".
if err, ok := err.(errPluginRequireExperimental); ok {
return nil, err
}
return nil, errPluginNotFound(name)
}
cmd := exec.Command(plugin.Path, args...)

View File

@ -23,6 +23,6 @@ type Metadata struct {
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Experimental specifies whether the plugin is experimental.
// Experimental plugins are not displayed on non-experimental CLIs.
// Deprecated: experimental features are now always enabled in the CLI
Experimental bool `json:",omitempty"`
}

View File

@ -35,7 +35,7 @@ type Plugin struct {
// non-recoverable error.
//
// nolint: gocyclo
func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plugin, error) {
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
@ -96,10 +96,6 @@ func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plu
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}
if p.Experimental && !allowExperimental {
p.Err = &pluginError{errPluginRequireExperimental(p.Name)}
return p, nil
}
if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil

View File

@ -152,16 +152,6 @@ func (cli *DockerCli) ClientInfo() ClientInfo {
}
func (cli *DockerCli) loadClientInfo() error {
var experimentalValue string
// Environment variable always overrides configuration
if experimentalValue = os.Getenv("DOCKER_CLI_EXPERIMENTAL"); experimentalValue == "" {
experimentalValue = cli.ConfigFile().Experimental
}
hasExperimental, err := isEnabled(experimentalValue)
if err != nil {
return errors.Wrap(err, "Experimental field")
}
var v string
if cli.client != nil {
v = cli.client.ClientVersion()
@ -170,7 +160,7 @@ func (cli *DockerCli) loadClientInfo() error {
}
cli.clientInfo = &ClientInfo{
DefaultVersion: v,
HasExperimental: hasExperimental,
HasExperimental: true,
}
return nil
}
@ -358,17 +348,6 @@ func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint
}, nil
}
func isEnabled(value string) (bool, error) {
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() {
ctx := context.Background()
if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") {
@ -471,6 +450,8 @@ type ServerInfo struct {
// ClientInfo stores details about the supported features of the client
type ClientInfo struct {
// Deprecated: experimental CLI features always enabled. This field is kept
// for backward-compatibility, and is always "true".
HasExperimental bool
DefaultVersion string
}

View File

@ -167,25 +167,24 @@ func TestInitializeFromClient(t *testing.T) {
}
}
// The CLI no longer disables/hides experimental CLI features, however, we need
// to verify that existing configuration files do not break
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,
},
}
@ -205,7 +204,8 @@ func TestExperimentalCLI(t *testing.T) {
cliconfig.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
assert.Check(t, is.Equal(testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental))
// For backward-compatibility, HasExperimental will always be "true"
assert.Check(t, is.Equal(true, cli.ClientInfo().HasExperimental))
})
}
}

View File

@ -103,7 +103,7 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
}
func (c *KubeCli) composeClient() (*Factory, error) {
return NewFactory(c.kubeNamespace, c.kubeConfig, c.clientSet, c.ClientInfo().HasExperimental)
return NewFactory(c.kubeNamespace, c.kubeConfig, c.clientSet)
}
func (c *KubeCli) checkHostsMatch() error {

View File

@ -18,11 +18,10 @@ type Factory struct {
coreClientSet corev1.CoreV1Interface
appsClientSet appsv1beta2.AppsV1beta2Interface
clientSet *kubeclient.Clientset
experimental bool
}
// NewFactory creates a kubernetes client factory
func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclient.Clientset, experimental bool) (*Factory, error) {
func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclient.Clientset) (*Factory, error) {
coreClientSet, err := corev1.NewForConfig(config)
if err != nil {
return nil, err
@ -39,7 +38,6 @@ func NewFactory(namespace string, config *restclient.Config, clientSet *kubeclie
coreClientSet: coreClientSet,
appsClientSet: appsClientSet,
clientSet: clientSet,
experimental: experimental,
}, nil
}
@ -85,7 +83,7 @@ func (s *Factory) DaemonSets() typesappsv1beta2.DaemonSetInterface {
// Stacks returns a client for Docker's Stack on Kubernetes
func (s *Factory) Stacks(allNamespaces bool) (StackClient, error) {
version, err := kubernetes.GetStackAPIVersion(s.clientSet.Discovery(), s.experimental)
version, err := kubernetes.GetStackAPIVersion(s.clientSet.Discovery())
if err != nil {
return nil, err
}

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"fmt"
"runtime"
"sort"
"strconv"
"text/tabwriter"
"text/template"
"time"
@ -83,7 +83,7 @@ type clientVersion struct {
Arch string
BuildTime string `json:",omitempty"`
Context string
Experimental bool
Experimental bool `json:",omitempty"` // Deprecated: experimental CLI features always enabled. This field is kept for backward-compatibility, and is always "true"
}
type kubernetesVersion struct {
@ -157,7 +157,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
BuildTime: reformatDate(version.BuildTime),
Os: runtime.GOOS,
Arch: arch(),
Experimental: dockerCli.ClientInfo().HasExperimental,
Experimental: true,
Context: dockerCli.CurrentContext(),
},
}
@ -199,7 +199,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
"Os": sv.Os,
"Arch": sv.Arch,
"BuildTime": reformatDate(vd.Server.BuildTime),
"Experimental": fmt.Sprintf("%t", sv.Experimental),
"Experimental": strconv.FormatBool(sv.Experimental),
},
})
}
@ -274,13 +274,13 @@ func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesV
logrus.Debugf("failed to get Kubernetes client: %s", err)
return &version
}
version.StackAPI = getStackVersion(kubeClient, dockerCli.ClientInfo().HasExperimental)
version.StackAPI = getStackVersion(kubeClient)
version.Kubernetes = getKubernetesServerVersion(kubeClient)
return &version
}
func getStackVersion(client *kubernetesClient.Clientset, experimental bool) string {
apiVersion, err := kubernetes.GetStackAPIVersion(client, experimental)
func getStackVersion(client *kubernetesClient.Clientset) string {
apiVersion, err := kubernetes.GetStackAPIVersion(client)
if err != nil {
logrus.Debugf("failed to get Stack API version: %s", err)
return "Unknown"

View File

@ -339,7 +339,6 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
buildKitDisabled = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return !v }
buildKitEnabled = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return v }
notExperimental = func(_ string) bool { return !details.ServerInfo().HasExperimental }
notExperimentalCLI = func(_ string) bool { return !details.ClientInfo().HasExperimental }
notOSType = func(v string) bool { return v != details.ServerInfo().OSType }
versionOlderThan = func(v string) bool { return versions.LessThan(details.Client().ClientVersion(), v) }
)
@ -359,7 +358,6 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
hideFlagIf(f, buildKitDisabled, "buildkit")
hideFlagIf(f, buildKitEnabled, "no-buildkit")
hideFlagIf(f, notExperimental, "experimental")
hideFlagIf(f, notExperimentalCLI, "experimentalCLI")
hideFlagIf(f, notOSType, "ostype")
hideFlagIf(f, versionOlderThan, "version")
})
@ -368,7 +366,6 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
hideSubcommandIf(subcmd, buildKitDisabled, "buildkit")
hideSubcommandIf(subcmd, buildKitEnabled, "no-buildkit")
hideSubcommandIf(subcmd, notExperimental, "experimental")
hideSubcommandIf(subcmd, notExperimentalCLI, "experimentalCLI")
hideSubcommandIf(subcmd, notOSType, "ostype")
hideSubcommandIf(subcmd, versionOlderThan, "version")
}
@ -417,9 +414,6 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
if _, ok := f.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
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 && !details.ClientInfo().HasExperimental {
errs = append(errs, fmt.Sprintf(`"--%s" is only supported on a Docker cli with experimental cli features enabled`, f.Name))
}
// buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case
})
if len(errs) > 0 {
@ -441,9 +435,6 @@ func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error {
if _, ok := curr.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
}
if _, ok := curr.Annotations["experimentalCLI"]; ok && !details.ClientInfo().HasExperimental {
return fmt.Errorf("%s is only supported on a Docker cli with experimental cli features enabled", cmd.CommandPath())
}
}
return nil
}

View File

@ -66,7 +66,6 @@ by the `docker` command line:
* `DOCKER_API_VERSION` The API version to use (e.g. `1.19`)
* `DOCKER_CONFIG` The location of your client configuration files.
* `DOCKER_CLI_EXPERIMENTAL` Enable experimental features for the cli (e.g. `enabled` or `disabled`)
* `DOCKER_HOST` Daemon socket to connect to.
* `DOCKER_STACK_ORCHESTRATOR` Configure the default orchestrator to use when using `docker stack` management commands.
* `DOCKER_CONTENT_TRUST` When set Docker uses notary to sign and verify images.
@ -317,27 +316,8 @@ Following is a sample `config.json` file:
### Experimental features
Experimental features provide early access to future product functionality.
These features are intended only for testing and feedback as they may change
between releases without warning or can be removed entirely from a future
release.
> Experimental features must not be used in production environments.
{: .warning }
To enable experimental features, edit the `config.json` file and set
`experimental` to `enabled`. The example below enables experimental features
in a `config.json` file that already enables a debug feature.
```json
{
"experimental": "enabled",
"debug": true
}
```
You can also enable experimental features from the Docker Desktop menu. See the
[Docker Desktop Getting Started page](https://docs.docker.com/docker-for-mac#experimental-features)
for more information.
These features are intended for testing and feedback, and they may change
between releases without warning or can be removed from a future release.
### Notary

View File

@ -24,18 +24,18 @@ const (
)
// GetStackAPIVersion returns the most appropriate stack API version installed.
func GetStackAPIVersion(serverGroups discovery.ServerGroupsInterface, experimental bool) (StackVersion, error) {
func GetStackAPIVersion(serverGroups discovery.ServerGroupsInterface) (StackVersion, error) {
groups, err := serverGroups.ServerGroups()
if err != nil {
return "", err
}
return getAPIVersion(groups, experimental)
return getAPIVersion(groups)
}
func getAPIVersion(groups *metav1.APIGroupList, experimental bool) (StackVersion, error) {
func getAPIVersion(groups *metav1.APIGroupList) (StackVersion, error) {
switch {
case experimental && findVersion(apiv1alpha3.SchemeGroupVersion, groups.Groups):
case findVersion(apiv1alpha3.SchemeGroupVersion, groups.Groups):
return StackAPIV1Alpha3, nil
case findVersion(apiv1beta2.SchemeGroupVersion, groups.Groups):
return StackAPIV1Beta2, nil

View File

@ -12,20 +12,18 @@ func TestGetStackAPIVersion(t *testing.T) {
var tests = []struct {
description string
groups *metav1.APIGroupList
experimental bool
err bool
expectedStack StackVersion
}{
{"no stack api", makeGroups(), false, true, ""},
{"v1beta1", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1"}}), false, false, StackAPIV1Beta1},
{"v1beta2", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta2"}}), false, false, StackAPIV1Beta2},
{"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2"}}), false, false, StackAPIV1Beta2},
{"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2", "v1alpha3"}}), false, false, StackAPIV1Beta2},
{"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2", "v1alpha3"}}), true, false, StackAPIV1Alpha3},
{"no stack api", makeGroups(), true, ""},
{"v1beta1", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1"}}), false, StackAPIV1Beta1},
{"v1beta2", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta2"}}), false, StackAPIV1Beta2},
{"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2"}}), false, StackAPIV1Beta2},
{"most recent has precedence", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1", "v1beta2", "v1alpha3"}}), false, StackAPIV1Alpha3},
}
for _, test := range tests {
version, err := getAPIVersion(test.groups, test.experimental)
version, err := getAPIVersion(test.groups)
if test.err {
assert.ErrorContains(t, err, "")
} else {