Merge pull request #48167 from dmcgowan/add-feature-flags-daemon

Add `--feature` to daemon flags
This commit is contained in:
Sebastiaan van Stijn
2024-09-12 19:15:19 +02:00
committed by GitHub
6 changed files with 264 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import (
"runtime"
"github.com/docker/docker/daemon/config"
dopts "github.com/docker/docker/internal/opts"
"github.com/docker/docker/opts"
"github.com/docker/docker/registry"
"github.com/spf13/pflag"
@@ -28,6 +29,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
flags.StringVar(&conf.ExecRoot, "exec-root", conf.ExecRoot, "Root directory for execution state files")
flags.StringVar(&conf.ContainerdAddr, "containerd", "", "containerd grpc address")
flags.BoolVar(&conf.CriContainerd, "cri-containerd", false, "start containerd with cri")
flags.Var(dopts.NewNamedSetOpts("features", conf.Features), "feature", "Enable feature in the daemon")
flags.Var(opts.NewNamedMapMapOpts("default-network-opts", conf.DefaultNetworkOpts, nil), "default-network-opt", "Default network options")
flags.IntVar(&conf.MTU, "mtu", conf.MTU, `Set the MTU for the default "bridge" network`)

View File

@@ -305,6 +305,7 @@ func New() (*Config, error) {
},
ContainerdNamespace: DefaultContainersNamespace,
ContainerdPluginNamespace: DefaultPluginNamespace,
Features: make(map[string]bool),
DefaultRuntime: StockRuntimeName,
MinAPIVersion: defaultMinAPIVersion,
},

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/docker/docker/api/types/container"
dopts "github.com/docker/docker/internal/opts"
"github.com/docker/docker/opts"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
@@ -121,6 +122,72 @@ func TestDaemonConfigurationMergeShmSize(t *testing.T) {
assert.Check(t, is.Equal(int64(expectedValue), cc.ShmSize.Value()))
}
func TestDaemonConfigurationFeatures(t *testing.T) {
tests := []struct {
name, config, flags string
expectedValue map[string]bool
expectedErr string
}{
{
name: "enable from file",
config: `{"features": {"containerd-snapshotter": true}}`,
expectedValue: map[string]bool{"containerd-snapshotter": true},
},
{
name: "enable from flags",
config: `{}`,
flags: "containerd-snapshotter=true",
expectedValue: map[string]bool{"containerd-snapshotter": true},
},
{
name: "disable from file",
config: `{"features": {"containerd-snapshotter": false}}`,
expectedValue: map[string]bool{"containerd-snapshotter": false},
},
{
name: "disable from flags",
config: `{}`,
flags: "containerd-snapshotter=false",
expectedValue: map[string]bool{"containerd-snapshotter": false},
},
{
name: "conflict",
config: `{"features": {"containerd-snapshotter": true}}`,
flags: "containerd-snapshotter=true",
expectedErr: `the following directives are specified both as a flag and in the configuration file: features: (from flag: map[containerd-snapshotter:true], from file: map[containerd-snapshotter:true])`,
},
{
name: "invalid config value",
config: `{"features": {"containerd-snapshotter": "not-a-boolean"}}`,
expectedErr: `json: cannot unmarshal string into Go struct field Config.features of type bool`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
c, err := New()
assert.NilError(t, err)
configFile := makeConfigFile(t, tc.config)
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags.Var(dopts.NewNamedSetOpts("features", c.Features), "feature", "Enable feature in the daemon")
if tc.flags != "" {
err = flags.Set("feature", tc.flags)
assert.NilError(t, err)
}
cc, err := MergeDaemonConfigurations(c, flags, configFile)
if tc.expectedErr != "" {
assert.Error(t, err, tc.expectedErr)
} else {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(tc.expectedValue, cc.Features))
}
})
}
}
func TestUnixGetInitPath(t *testing.T) {
testCases := []struct {
config *Config

View File

@@ -179,6 +179,74 @@ func TestConfigDaemonSeccompProfiles(t *testing.T) {
}
}
func TestDaemonConfigFeatures(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
ctx := testutil.StartSpan(baseContext, t)
d := daemon.New(t)
dockerBinary, err := d.BinaryPath()
assert.NilError(t, err)
params := []string{"--validate", "--config-file"}
dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
if dest == "" {
dest = os.Getenv("DEST")
}
testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
const (
validOut = "configuration OK"
failedOut = "unable to configure the Docker daemon with file"
)
tests := []struct {
name string
args []string
expectedOut string
}{
{
name: "config with no content",
args: append(params, filepath.Join(testdata, "empty-config-1.json")),
expectedOut: validOut,
},
{
name: "config with {}",
args: append(params, filepath.Join(testdata, "empty-config-2.json")),
expectedOut: validOut,
},
{
name: "invalid config",
args: append(params, filepath.Join(testdata, "invalid-config-1.json")),
expectedOut: failedOut,
},
{
name: "malformed config",
args: append(params, filepath.Join(testdata, "malformed-config.json")),
expectedOut: failedOut,
},
{
name: "valid config",
args: append(params, filepath.Join(testdata, "valid-config-1.json")),
expectedOut: validOut,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_ = testutil.StartSpan(ctx, t)
cmd := exec.Command(dockerBinary, tc.args...)
out, err := cmd.CombinedOutput()
assert.Check(t, is.Contains(string(out), tc.expectedOut))
if tc.expectedOut == failedOut {
assert.ErrorContains(t, err, "", "expected an error, but got none")
} else {
assert.NilError(t, err)
}
})
}
}
func TestDaemonProxy(t *testing.T) {
skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment")

80
internal/opts/opts.go Normal file
View File

@@ -0,0 +1,80 @@
package opts
import (
"fmt"
"strconv"
"strings"
"github.com/docker/docker/opts"
)
// SetOpts holds a map of values and a validation function.
type SetOpts struct {
values map[string]bool
}
// Set validates if needed the input value and add it to the
// internal map, by splitting on '='.
func (opts *SetOpts) Set(value string) error {
k, v, found := strings.Cut(value, "=")
var isSet bool
if !found {
isSet = true
k = value
} else {
var err error
isSet, err = strconv.ParseBool(v)
if err != nil {
return err
}
}
opts.values[k] = isSet
return nil
}
// GetAll returns the values of SetOpts as a map.
func (opts *SetOpts) GetAll() map[string]bool {
return opts.values
}
func (opts *SetOpts) String() string {
return fmt.Sprintf("%v", opts.values)
}
// Type returns a string name for this Option type
func (opts *SetOpts) Type() string {
return "map"
}
// NewSetOpts creates a new SetOpts with the specified set of values as a map of string to bool.
func NewSetOpts(values map[string]bool) *SetOpts {
if values == nil {
values = make(map[string]bool)
}
return &SetOpts{
values: values,
}
}
// NamedSetOpts is a SetOpts struct with a configuration name.
// This struct is useful to keep reference to the assigned
// field name in the internal configuration struct.
type NamedSetOpts struct {
SetOpts
name string
}
var _ opts.NamedOption = &NamedSetOpts{}
// NewNamedSetOpts creates a reference to a new NamedSetOpts struct.
func NewNamedSetOpts(name string, values map[string]bool) *NamedSetOpts {
return &NamedSetOpts{
SetOpts: *NewSetOpts(values),
name: name,
}
}
// Name returns the name of the NamedSetOpts in the configuration.
func (o *NamedSetOpts) Name() string {
return o.name
}

View File

@@ -0,0 +1,46 @@
package opts
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestSetOpts(t *testing.T) {
tmpMap := make(map[string]bool)
o := NewSetOpts(tmpMap)
assert.NilError(t, o.Set("feature-a=1"))
assert.NilError(t, o.Set("feature-b=true"))
assert.NilError(t, o.Set("feature-c=0"))
assert.NilError(t, o.Set("feature-d=false"))
expected := "map[feature-a:true feature-b:true feature-c:false feature-d:false]"
assert.Check(t, is.Equal(expected, o.String()))
expectedValue := map[string]bool{"feature-a": true, "feature-b": true, "feature-c": false, "feature-d": false}
assert.Check(t, is.DeepEqual(expectedValue, o.GetAll()))
err := o.Set("feature=not-a-bool")
assert.Check(t, is.Error(err, `strconv.ParseBool: parsing "not-a-bool": invalid syntax`))
}
func TestNamedSetOpts(t *testing.T) {
tmpMap := make(map[string]bool)
o := NewNamedSetOpts("features", tmpMap)
assert.Check(t, is.Equal("features", o.Name()))
assert.NilError(t, o.Set("feature-a=1"))
assert.NilError(t, o.Set("feature-b=true"))
assert.NilError(t, o.Set("feature-c=0"))
assert.NilError(t, o.Set("feature-d=false"))
expected := "map[feature-a:true feature-b:true feature-c:false feature-d:false]"
assert.Check(t, is.Equal(expected, o.String()))
expectedValue := map[string]bool{"feature-a": true, "feature-b": true, "feature-c": false, "feature-d": false}
assert.Check(t, is.DeepEqual(expectedValue, o.GetAll()))
err := o.Set("feature=not-a-bool")
assert.Check(t, is.Error(err, `strconv.ParseBool: parsing "not-a-bool": invalid syntax`))
}