Skip to content

Commit f13c082

Browse files
dmcgowanthaJeztah
authored andcommitted
Add feature to daemon flags
Signed-off-by: Derek McGowan <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent b3c7750 commit f13c082

6 files changed

Lines changed: 264 additions & 0 deletions

File tree

cmd/dockerd/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"runtime"
55

66
"github.com/docker/docker/daemon/config"
7+
dopts "github.com/docker/docker/internal/opts"
78
"github.com/docker/docker/opts"
89
"github.com/docker/docker/registry"
910
"github.com/spf13/pflag"
@@ -28,6 +29,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
2829
flags.StringVar(&conf.ExecRoot, "exec-root", conf.ExecRoot, "Root directory for execution state files")
2930
flags.StringVar(&conf.ContainerdAddr, "containerd", "", "containerd grpc address")
3031
flags.BoolVar(&conf.CriContainerd, "cri-containerd", false, "start containerd with cri")
32+
flags.Var(dopts.NewNamedSetOpts("features", conf.Features), "feature", "Enable feature in the daemon")
3133

3234
flags.Var(opts.NewNamedMapMapOpts("default-network-opts", conf.DefaultNetworkOpts, nil), "default-network-opt", "Default network options")
3335
flags.IntVar(&conf.MTU, "mtu", conf.MTU, `Set the MTU for the default "bridge" network`)

daemon/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ func New() (*Config, error) {
305305
},
306306
ContainerdNamespace: DefaultContainersNamespace,
307307
ContainerdPluginNamespace: DefaultPluginNamespace,
308+
Features: make(map[string]bool),
308309
DefaultRuntime: StockRuntimeName,
309310
MinAPIVersion: defaultMinAPIVersion,
310311
},

daemon/config/config_linux_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/docker/docker/api/types/container"
7+
dopts "github.com/docker/docker/internal/opts"
78
"github.com/docker/docker/opts"
89
"github.com/spf13/pflag"
910
"gotest.tools/v3/assert"
@@ -121,6 +122,72 @@ func TestDaemonConfigurationMergeShmSize(t *testing.T) {
121122
assert.Check(t, is.Equal(int64(expectedValue), cc.ShmSize.Value()))
122123
}
123124

125+
func TestDaemonConfigurationFeatures(t *testing.T) {
126+
tests := []struct {
127+
name, config, flags string
128+
expectedValue map[string]bool
129+
expectedErr string
130+
}{
131+
{
132+
name: "enable from file",
133+
config: `{"features": {"containerd-snapshotter": true}}`,
134+
expectedValue: map[string]bool{"containerd-snapshotter": true},
135+
},
136+
{
137+
name: "enable from flags",
138+
config: `{}`,
139+
flags: "containerd-snapshotter=true",
140+
expectedValue: map[string]bool{"containerd-snapshotter": true},
141+
},
142+
{
143+
name: "disable from file",
144+
config: `{"features": {"containerd-snapshotter": false}}`,
145+
expectedValue: map[string]bool{"containerd-snapshotter": false},
146+
},
147+
{
148+
name: "disable from flags",
149+
config: `{}`,
150+
flags: "containerd-snapshotter=false",
151+
expectedValue: map[string]bool{"containerd-snapshotter": false},
152+
},
153+
{
154+
name: "conflict",
155+
config: `{"features": {"containerd-snapshotter": true}}`,
156+
flags: "containerd-snapshotter=true",
157+
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])`,
158+
},
159+
{
160+
name: "invalid config value",
161+
config: `{"features": {"containerd-snapshotter": "not-a-boolean"}}`,
162+
expectedErr: `json: cannot unmarshal string into Go struct field Config.features of type bool`,
163+
},
164+
}
165+
166+
for _, tc := range tests {
167+
tc := tc
168+
169+
t.Run(tc.name, func(t *testing.T) {
170+
c, err := New()
171+
assert.NilError(t, err)
172+
173+
configFile := makeConfigFile(t, tc.config)
174+
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
175+
flags.Var(dopts.NewNamedSetOpts("features", c.Features), "feature", "Enable feature in the daemon")
176+
if tc.flags != "" {
177+
err = flags.Set("feature", tc.flags)
178+
assert.NilError(t, err)
179+
}
180+
cc, err := MergeDaemonConfigurations(c, flags, configFile)
181+
if tc.expectedErr != "" {
182+
assert.Error(t, err, tc.expectedErr)
183+
} else {
184+
assert.NilError(t, err)
185+
assert.Check(t, is.DeepEqual(tc.expectedValue, cc.Features))
186+
}
187+
})
188+
}
189+
}
190+
124191
func TestUnixGetInitPath(t *testing.T) {
125192
testCases := []struct {
126193
config *Config

integration/daemon/daemon_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,74 @@ func TestConfigDaemonSeccompProfiles(t *testing.T) {
179179
}
180180
}
181181

182+
func TestDaemonConfigFeatures(t *testing.T) {
183+
skip.If(t, runtime.GOOS == "windows")
184+
ctx := testutil.StartSpan(baseContext, t)
185+
186+
d := daemon.New(t)
187+
dockerBinary, err := d.BinaryPath()
188+
assert.NilError(t, err)
189+
params := []string{"--validate", "--config-file"}
190+
191+
dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
192+
if dest == "" {
193+
dest = os.Getenv("DEST")
194+
}
195+
testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
196+
197+
const (
198+
validOut = "configuration OK"
199+
failedOut = "unable to configure the Docker daemon with file"
200+
)
201+
202+
tests := []struct {
203+
name string
204+
args []string
205+
expectedOut string
206+
}{
207+
{
208+
name: "config with no content",
209+
args: append(params, filepath.Join(testdata, "empty-config-1.json")),
210+
expectedOut: validOut,
211+
},
212+
{
213+
name: "config with {}",
214+
args: append(params, filepath.Join(testdata, "empty-config-2.json")),
215+
expectedOut: validOut,
216+
},
217+
{
218+
name: "invalid config",
219+
args: append(params, filepath.Join(testdata, "invalid-config-1.json")),
220+
expectedOut: failedOut,
221+
},
222+
{
223+
name: "malformed config",
224+
args: append(params, filepath.Join(testdata, "malformed-config.json")),
225+
expectedOut: failedOut,
226+
},
227+
{
228+
name: "valid config",
229+
args: append(params, filepath.Join(testdata, "valid-config-1.json")),
230+
expectedOut: validOut,
231+
},
232+
}
233+
for _, tc := range tests {
234+
tc := tc
235+
t.Run(tc.name, func(t *testing.T) {
236+
t.Parallel()
237+
_ = testutil.StartSpan(ctx, t)
238+
cmd := exec.Command(dockerBinary, tc.args...)
239+
out, err := cmd.CombinedOutput()
240+
assert.Check(t, is.Contains(string(out), tc.expectedOut))
241+
if tc.expectedOut == failedOut {
242+
assert.ErrorContains(t, err, "", "expected an error, but got none")
243+
} else {
244+
assert.NilError(t, err)
245+
}
246+
})
247+
}
248+
}
249+
182250
func TestDaemonProxy(t *testing.T) {
183251
skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
184252
skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment")

internal/opts/opts.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package opts
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/docker/docker/opts"
9+
)
10+
11+
// SetOpts holds a map of values and a validation function.
12+
type SetOpts struct {
13+
values map[string]bool
14+
}
15+
16+
// Set validates if needed the input value and add it to the
17+
// internal map, by splitting on '='.
18+
func (opts *SetOpts) Set(value string) error {
19+
k, v, found := strings.Cut(value, "=")
20+
var isSet bool
21+
if !found {
22+
isSet = true
23+
k = value
24+
} else {
25+
var err error
26+
isSet, err = strconv.ParseBool(v)
27+
if err != nil {
28+
return err
29+
}
30+
}
31+
opts.values[k] = isSet
32+
return nil
33+
}
34+
35+
// GetAll returns the values of SetOpts as a map.
36+
func (opts *SetOpts) GetAll() map[string]bool {
37+
return opts.values
38+
}
39+
40+
func (opts *SetOpts) String() string {
41+
return fmt.Sprintf("%v", opts.values)
42+
}
43+
44+
// Type returns a string name for this Option type
45+
func (opts *SetOpts) Type() string {
46+
return "map"
47+
}
48+
49+
// NewSetOpts creates a new SetOpts with the specified set of values as a map of string to bool.
50+
func NewSetOpts(values map[string]bool) *SetOpts {
51+
if values == nil {
52+
values = make(map[string]bool)
53+
}
54+
return &SetOpts{
55+
values: values,
56+
}
57+
}
58+
59+
// NamedSetOpts is a SetOpts struct with a configuration name.
60+
// This struct is useful to keep reference to the assigned
61+
// field name in the internal configuration struct.
62+
type NamedSetOpts struct {
63+
SetOpts
64+
name string
65+
}
66+
67+
var _ opts.NamedOption = &NamedSetOpts{}
68+
69+
// NewNamedSetOpts creates a reference to a new NamedSetOpts struct.
70+
func NewNamedSetOpts(name string, values map[string]bool) *NamedSetOpts {
71+
return &NamedSetOpts{
72+
SetOpts: *NewSetOpts(values),
73+
name: name,
74+
}
75+
}
76+
77+
// Name returns the name of the NamedSetOpts in the configuration.
78+
func (o *NamedSetOpts) Name() string {
79+
return o.name
80+
}

internal/opts/opts_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package opts
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
is "gotest.tools/v3/assert/cmp"
8+
)
9+
10+
func TestSetOpts(t *testing.T) {
11+
tmpMap := make(map[string]bool)
12+
o := NewSetOpts(tmpMap)
13+
assert.NilError(t, o.Set("feature-a=1"))
14+
assert.NilError(t, o.Set("feature-b=true"))
15+
assert.NilError(t, o.Set("feature-c=0"))
16+
assert.NilError(t, o.Set("feature-d=false"))
17+
18+
expected := "map[feature-a:true feature-b:true feature-c:false feature-d:false]"
19+
assert.Check(t, is.Equal(expected, o.String()))
20+
21+
expectedValue := map[string]bool{"feature-a": true, "feature-b": true, "feature-c": false, "feature-d": false}
22+
assert.Check(t, is.DeepEqual(expectedValue, o.GetAll()))
23+
24+
err := o.Set("feature=not-a-bool")
25+
assert.Check(t, is.Error(err, `strconv.ParseBool: parsing "not-a-bool": invalid syntax`))
26+
}
27+
28+
func TestNamedSetOpts(t *testing.T) {
29+
tmpMap := make(map[string]bool)
30+
o := NewNamedSetOpts("features", tmpMap)
31+
assert.Check(t, is.Equal("features", o.Name()))
32+
33+
assert.NilError(t, o.Set("feature-a=1"))
34+
assert.NilError(t, o.Set("feature-b=true"))
35+
assert.NilError(t, o.Set("feature-c=0"))
36+
assert.NilError(t, o.Set("feature-d=false"))
37+
38+
expected := "map[feature-a:true feature-b:true feature-c:false feature-d:false]"
39+
assert.Check(t, is.Equal(expected, o.String()))
40+
41+
expectedValue := map[string]bool{"feature-a": true, "feature-b": true, "feature-c": false, "feature-d": false}
42+
assert.Check(t, is.DeepEqual(expectedValue, o.GetAll()))
43+
44+
err := o.Set("feature=not-a-bool")
45+
assert.Check(t, is.Error(err, `strconv.ParseBool: parsing "not-a-bool": invalid syntax`))
46+
}

0 commit comments

Comments
 (0)