mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Merge pull request #48167 from dmcgowan/add-feature-flags-daemon
Add `--feature` to daemon flags
This commit is contained in:
@@ -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`)
|
||||
|
||||
@@ -305,6 +305,7 @@ func New() (*Config, error) {
|
||||
},
|
||||
ContainerdNamespace: DefaultContainersNamespace,
|
||||
ContainerdPluginNamespace: DefaultPluginNamespace,
|
||||
Features: make(map[string]bool),
|
||||
DefaultRuntime: StockRuntimeName,
|
||||
MinAPIVersion: defaultMinAPIVersion,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
80
internal/opts/opts.go
Normal 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
|
||||
}
|
||||
46
internal/opts/opts_test.go
Normal file
46
internal/opts/opts_test.go
Normal 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`))
|
||||
}
|
||||
Reference in New Issue
Block a user