Browse code

api/types/container: add RestartPolicyMode type and enum

Also move the validation function to live with the type definition,
which allows it to be used outside of the daemon as well.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2022/12/28 19:17:59
Showing 12 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+package container
1
+
2
+type errInvalidParameter struct{ error }
3
+
4
+func (e *errInvalidParameter) InvalidParameter() {}
5
+
6
+func (e *errInvalidParameter) Unwrap() error {
7
+	return e.error
8
+}
... ...
@@ -1,6 +1,7 @@
1 1
 package container // import "github.com/docker/docker/api/types/container"
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"strings"
5 6
 
6 7
 	"github.com/docker/docker/api/types/blkiodev"
... ...
@@ -271,33 +272,42 @@ type DeviceMapping struct {
271 271
 
272 272
 // RestartPolicy represents the restart policies of the container.
273 273
 type RestartPolicy struct {
274
-	Name              string
274
+	Name              RestartPolicyMode
275 275
 	MaximumRetryCount int
276 276
 }
277 277
 
278
+type RestartPolicyMode string
279
+
280
+const (
281
+	RestartPolicyDisabled      RestartPolicyMode = "no"
282
+	RestartPolicyAlways        RestartPolicyMode = "always"
283
+	RestartPolicyOnFailure     RestartPolicyMode = "on-failure"
284
+	RestartPolicyUnlessStopped RestartPolicyMode = "unless-stopped"
285
+)
286
+
278 287
 // IsNone indicates whether the container has the "no" restart policy.
279 288
 // This means the container will not automatically restart when exiting.
280 289
 func (rp *RestartPolicy) IsNone() bool {
281
-	return rp.Name == "no" || rp.Name == ""
290
+	return rp.Name == RestartPolicyDisabled || rp.Name == ""
282 291
 }
283 292
 
284 293
 // IsAlways indicates whether the container has the "always" restart policy.
285 294
 // This means the container will automatically restart regardless of the exit status.
286 295
 func (rp *RestartPolicy) IsAlways() bool {
287
-	return rp.Name == "always"
296
+	return rp.Name == RestartPolicyAlways
288 297
 }
289 298
 
290 299
 // IsOnFailure indicates whether the container has the "on-failure" restart policy.
291 300
 // This means the container will automatically restart of exiting with a non-zero exit status.
292 301
 func (rp *RestartPolicy) IsOnFailure() bool {
293
-	return rp.Name == "on-failure"
302
+	return rp.Name == RestartPolicyOnFailure
294 303
 }
295 304
 
296 305
 // IsUnlessStopped indicates whether the container has the
297 306
 // "unless-stopped" restart policy. This means the container will
298 307
 // automatically restart unless user has put it to stopped state.
299 308
 func (rp *RestartPolicy) IsUnlessStopped() bool {
300
-	return rp.Name == "unless-stopped"
309
+	return rp.Name == RestartPolicyUnlessStopped
301 310
 }
302 311
 
303 312
 // IsSame compares two RestartPolicy to see if they are the same
... ...
@@ -305,6 +315,29 @@ func (rp *RestartPolicy) IsSame(tp *RestartPolicy) bool {
305 305
 	return rp.Name == tp.Name && rp.MaximumRetryCount == tp.MaximumRetryCount
306 306
 }
307 307
 
308
+// ValidateRestartPolicy validates the given RestartPolicy.
309
+func ValidateRestartPolicy(policy RestartPolicy) error {
310
+	switch policy.Name {
311
+	case RestartPolicyAlways, RestartPolicyUnlessStopped, RestartPolicyDisabled:
312
+		if policy.MaximumRetryCount != 0 {
313
+			return &errInvalidParameter{fmt.Errorf("invalid restart policy: maximum retry count cannot be used with restart policy '%s'", policy.Name)}
314
+		}
315
+		return nil
316
+	case RestartPolicyOnFailure:
317
+		if policy.MaximumRetryCount < 0 {
318
+			return &errInvalidParameter{fmt.Errorf("invalid restart policy: maximum retry count cannot be negative")}
319
+		}
320
+		return nil
321
+	case "":
322
+		// Versions before v25.0.0 created an empty restart-policy "name" as
323
+		// default. Allow an empty name with "any" MaximumRetryCount for
324
+		// backward-compatibility.
325
+		return nil
326
+	default:
327
+		return &errInvalidParameter{fmt.Errorf("invalid restart policy: '%s'", policy.Name)}
328
+	}
329
+}
330
+
308 331
 // LogMode is a type to define the available modes for logging
309 332
 // These modes affect how logs are handled when log messages start piling up.
310 333
 type LogMode string
311 334
new file mode 100644
... ...
@@ -0,0 +1,105 @@
0
+package container
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/errdefs"
6
+	"gotest.tools/v3/assert"
7
+	is "gotest.tools/v3/assert/cmp"
8
+)
9
+
10
+func TestValidateRestartPolicy(t *testing.T) {
11
+	tests := []struct {
12
+		name        string
13
+		input       RestartPolicy
14
+		expectedErr string
15
+	}{
16
+		{
17
+			name:  "empty",
18
+			input: RestartPolicy{},
19
+		},
20
+		{
21
+			name:        "empty with invalid MaxRestartCount (for backward compatibility)",
22
+			input:       RestartPolicy{MaximumRetryCount: 123},
23
+			expectedErr: "", // Allowed for backward compatibility
24
+		},
25
+		{
26
+			name:        "empty with negative MaxRestartCount)",
27
+			input:       RestartPolicy{MaximumRetryCount: -123},
28
+			expectedErr: "", // Allowed for backward compatibility
29
+		},
30
+		{
31
+			name:  "always",
32
+			input: RestartPolicy{Name: RestartPolicyAlways},
33
+		},
34
+		{
35
+			name:        "always with MaxRestartCount",
36
+			input:       RestartPolicy{Name: RestartPolicyAlways, MaximumRetryCount: 123},
37
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'always'",
38
+		},
39
+		{
40
+			name:        "always with negative MaxRestartCount",
41
+			input:       RestartPolicy{Name: RestartPolicyAlways, MaximumRetryCount: -123},
42
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'always'",
43
+		},
44
+		{
45
+			name:  "unless-stopped",
46
+			input: RestartPolicy{Name: RestartPolicyUnlessStopped},
47
+		},
48
+		{
49
+			name:        "unless-stopped with MaxRestartCount",
50
+			input:       RestartPolicy{Name: RestartPolicyUnlessStopped, MaximumRetryCount: 123},
51
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'unless-stopped'",
52
+		},
53
+		{
54
+			name:        "unless-stopped with negative MaxRestartCount",
55
+			input:       RestartPolicy{Name: RestartPolicyUnlessStopped, MaximumRetryCount: -123},
56
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'unless-stopped'",
57
+		},
58
+		{
59
+			name:  "disabled",
60
+			input: RestartPolicy{Name: RestartPolicyDisabled},
61
+		},
62
+		{
63
+			name:        "disabled with MaxRestartCount",
64
+			input:       RestartPolicy{Name: RestartPolicyDisabled, MaximumRetryCount: 123},
65
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'no'",
66
+		},
67
+		{
68
+			name:        "disabled with negative MaxRestartCount",
69
+			input:       RestartPolicy{Name: RestartPolicyDisabled, MaximumRetryCount: -123},
70
+			expectedErr: "invalid restart policy: maximum retry count cannot be used with restart policy 'no'",
71
+		},
72
+		{
73
+			name:  "on-failure",
74
+			input: RestartPolicy{Name: RestartPolicyOnFailure},
75
+		},
76
+		{
77
+			name:  "on-failure with MaxRestartCount",
78
+			input: RestartPolicy{Name: RestartPolicyOnFailure, MaximumRetryCount: 123},
79
+		},
80
+		{
81
+			name:        "on-failure with negative MaxRestartCount",
82
+			input:       RestartPolicy{Name: RestartPolicyOnFailure, MaximumRetryCount: -123},
83
+			expectedErr: "invalid restart policy: maximum retry count cannot be negative",
84
+		},
85
+		{
86
+			name:        "unknown policy",
87
+			input:       RestartPolicy{Name: "I do not exist"},
88
+			expectedErr: "invalid restart policy: 'I do not exist'",
89
+		},
90
+	}
91
+
92
+	for _, tc := range tests {
93
+		tc := tc
94
+		t.Run(tc.name, func(t *testing.T) {
95
+			err := ValidateRestartPolicy(tc.input)
96
+			if tc.expectedErr == "" {
97
+				assert.Check(t, err)
98
+			} else {
99
+				assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
100
+				assert.Check(t, is.Error(err, tc.expectedErr))
101
+			}
102
+		})
103
+	}
104
+}
... ...
@@ -221,7 +221,7 @@ func TestRestartPolicy(t *testing.T) {
221 221
 		{Name: "on-failure", MaximumRetryCount: 0}: {none: false, always: false, onFailure: true},
222 222
 	}
223 223
 	for policy, expected := range policies {
224
-		t.Run("policy="+policy.Name, func(t *testing.T) {
224
+		t.Run("policy="+string(policy.Name), func(t *testing.T) {
225 225
 			assert.Check(t, is.Equal(policy.IsNone(), expected.none))
226 226
 			assert.Check(t, is.Equal(policy.IsAlways(), expected.always))
227 227
 			assert.Check(t, is.Equal(policy.IsOnFailure(), expected.onFailure))
... ...
@@ -298,7 +298,7 @@ func validateHostConfig(hostConfig *containertypes.HostConfig) error {
298 298
 	if err := validatePortBindings(hostConfig.PortBindings); err != nil {
299 299
 		return err
300 300
 	}
301
-	if err := validateRestartPolicy(hostConfig.RestartPolicy); err != nil {
301
+	if err := containertypes.ValidateRestartPolicy(hostConfig.RestartPolicy); err != nil {
302 302
 		return err
303 303
 	}
304 304
 	if err := validateCapabilities(hostConfig); err != nil {
... ...
@@ -362,25 +362,6 @@ func validatePortBindings(ports nat.PortMap) error {
362 362
 	return nil
363 363
 }
364 364
 
365
-func validateRestartPolicy(policy containertypes.RestartPolicy) error {
366
-	switch policy.Name {
367
-	case "always", "unless-stopped", "no":
368
-		if policy.MaximumRetryCount != 0 {
369
-			return errors.Errorf("maximum retry count cannot be used with restart policy '%s'", policy.Name)
370
-		}
371
-	case "on-failure":
372
-		if policy.MaximumRetryCount < 0 {
373
-			return errors.Errorf("maximum retry count cannot be negative")
374
-		}
375
-	case "":
376
-		// do nothing
377
-		return nil
378
-	default:
379
-		return errors.Errorf("invalid restart policy '%s'", policy.Name)
380
-	}
381
-	return nil
382
-}
383
-
384 365
 // translateWorkingDir translates the working-dir for the target platform,
385 366
 // and returns an error if the given path is not an absolute path.
386 367
 func translateWorkingDir(config *containertypes.Config) error {
... ...
@@ -63,6 +63,16 @@ func (daemon *Daemon) containerCreate(ctx context.Context, daemonCfg *configStor
63 63
 		return containertypes.CreateResponse{}, errdefs.InvalidParameter(errors.New("Config cannot be empty in order to create a container"))
64 64
 	}
65 65
 
66
+	// Normalize some defaults. Doing this "ad-hoc" here for now, as there's
67
+	// only one field to migrate, but we should consider having a better
68
+	// location for this (and decide where in the flow would be most appropriate).
69
+	//
70
+	// TODO(thaJeztah): we should have a more visible, more canonical location for this.
71
+	if opts.params.HostConfig != nil && opts.params.HostConfig.RestartPolicy.Name == "" {
72
+		// Set the default restart-policy ("none") if no restart-policy was set.
73
+		opts.params.HostConfig.RestartPolicy.Name = containertypes.RestartPolicyDisabled
74
+	}
75
+
66 76
 	warnings, err := daemon.verifyContainerSettings(daemonCfg, opts.params.HostConfig, opts.params.Config, false)
67 77
 	if err != nil {
68 78
 		return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
... ...
@@ -338,8 +338,21 @@ func (daemon *Daemon) restore(cfg *configStore) error {
338 338
 
339 339
 			baseLogger := log.G(context.TODO()).WithField("container", c.ID)
340 340
 
341
+			// Migrate containers that don't have the default ("no") restart-policy set.
342
+			// The RestartPolicy.Name field may be empty for containers that were
343
+			// created with versions before v25.0.0.
344
+			//
345
+			// We also need to set the MaximumRetryCount to 0, to prevent
346
+			// validation from failing (MaximumRetryCount is not allowed if
347
+			// no restart-policy ("none") is set).
348
+			if c.HostConfig != nil && c.HostConfig.RestartPolicy.Name == "" {
349
+				baseLogger.WithError(err).Debug("migrated restart-policy")
350
+				c.HostConfig.RestartPolicy.Name = containertypes.RestartPolicyDisabled
351
+				c.HostConfig.RestartPolicy.MaximumRetryCount = 0
352
+			}
353
+
341 354
 			if err := daemon.checkpointAndSave(c); err != nil {
342
-				baseLogger.WithError(err).Error("error saving backported mountspec to disk")
355
+				baseLogger.WithError(err).Error("failed to save migrated container config to disk")
343 356
 			}
344 357
 
345 358
 			daemon.setStateCounter(c)
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"time"
11 11
 
12 12
 	"github.com/docker/docker/api/types"
13
-	containerapi "github.com/docker/docker/api/types/container"
13
+	containertypes "github.com/docker/docker/api/types/container"
14 14
 	realcontainer "github.com/docker/docker/container"
15 15
 	"github.com/docker/docker/integration/internal/container"
16 16
 	"github.com/docker/docker/testutil/daemon"
... ...
@@ -101,7 +101,7 @@ func TestDaemonRestartIpcMode(t *testing.T) {
101 101
 	// check the container is created with private ipc mode as per daemon default
102 102
 	cID := container.Run(ctx, t, c,
103 103
 		container.WithCmd("top"),
104
-		container.WithRestartPolicy("always"),
104
+		container.WithRestartPolicy(containertypes.RestartPolicyAlways),
105 105
 	)
106 106
 	defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
107 107
 
... ...
@@ -199,7 +199,7 @@ func TestRestartDaemonWithRestartingContainer(t *testing.T) {
199 199
 	// Just create the container, no need to start it to be started.
200 200
 	// We really want to make sure there is no process running when docker starts back up.
201 201
 	// We will manipulate the on disk state later
202
-	id := container.Create(ctx, t, apiClient, container.WithRestartPolicy("always"), container.WithCmd("/bin/sh", "-c", "exit 1"))
202
+	id := container.Create(ctx, t, apiClient, container.WithRestartPolicy(containertypes.RestartPolicyAlways), container.WithCmd("/bin/sh", "-c", "exit 1"))
203 203
 
204 204
 	d.Stop(t)
205 205
 
... ...
@@ -212,7 +212,7 @@ func TestRestartDaemonWithRestartingContainer(t *testing.T) {
212 212
 
213 213
 	ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
214 214
 	defer cancel()
215
-	chOk, chErr := apiClient.ContainerWait(ctxTimeout, id, containerapi.WaitConditionNextExit)
215
+	chOk, chErr := apiClient.ContainerWait(ctxTimeout, id, containertypes.WaitConditionNextExit)
216 216
 	select {
217 217
 	case <-chOk:
218 218
 	case err := <-chErr:
... ...
@@ -284,6 +284,6 @@ func TestHardRestartWhenContainerIsRunning(t *testing.T) {
284 284
 		}
285 285
 
286 286
 		stopTimeout := 0
287
-		assert.Assert(t, apiClient.ContainerStop(ctx, onFailure, containerapi.StopOptions{Timeout: &stopTimeout}))
287
+		assert.Assert(t, apiClient.ContainerStop(ctx, onFailure, containertypes.StopOptions{Timeout: &stopTimeout}))
288 288
 	})
289 289
 }
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"testing"
7 7
 	"time"
8 8
 
9
+	containertypes "github.com/docker/docker/api/types/container"
9 10
 	"github.com/docker/docker/client"
10 11
 	"github.com/docker/docker/integration/internal/container"
11 12
 	"github.com/docker/docker/testutil/request"
... ...
@@ -112,7 +113,7 @@ func TestKillWithStopSignalAndRestartPolicies(t *testing.T) {
112 112
 		t.Run(tc.doc, func(t *testing.T) {
113 113
 			ctx := context.Background()
114 114
 			id := container.Run(ctx, t, apiClient,
115
-				container.WithRestartPolicy("always"),
115
+				container.WithRestartPolicy(containertypes.RestartPolicyAlways),
116 116
 				func(c *container.TestContainerConfig) {
117 117
 					c.Config.StopSignal = tc.stopsignal
118 118
 				})
... ...
@@ -24,7 +24,7 @@ func TestStopContainerWithRestartPolicyAlways(t *testing.T) {
24 24
 		container.Run(ctx, t, apiClient,
25 25
 			container.WithName(name),
26 26
 			container.WithCmd("false"),
27
-			container.WithRestartPolicy("always"),
27
+			container.WithRestartPolicy(containertypes.RestartPolicyAlways),
28 28
 		)
29 29
 	}
30 30
 
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"testing"
14 14
 
15 15
 	"github.com/docker/docker/api/types"
16
+	containertypes "github.com/docker/docker/api/types/container"
16 17
 	"github.com/docker/docker/api/types/mount"
17 18
 	"github.com/docker/docker/api/types/volume"
18 19
 	"github.com/docker/docker/daemon/config"
... ...
@@ -380,9 +381,9 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
380 380
 	c := d.NewClientT(t)
381 381
 	ctx := context.Background()
382 382
 
383
-	runTest := func(t *testing.T, policy string) {
384
-		t.Run(policy, func(t *testing.T) {
385
-			volName := "test-live-restore-volume-references-" + policy
383
+	runTest := func(t *testing.T, policy containertypes.RestartPolicyMode) {
384
+		t.Run(string(policy), func(t *testing.T) {
385
+			volName := "test-live-restore-volume-references-" + string(policy)
386 386
 			_, err := c.VolumeCreate(ctx, volume.CreateOptions{Name: volName})
387 387
 			assert.NilError(t, err)
388 388
 
... ...
@@ -408,10 +409,10 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
408 408
 	}
409 409
 
410 410
 	t.Run("restartPolicy", func(t *testing.T) {
411
-		runTest(t, "always")
412
-		runTest(t, "unless-stopped")
413
-		runTest(t, "on-failure")
414
-		runTest(t, "no")
411
+		runTest(t, containertypes.RestartPolicyAlways)
412
+		runTest(t, containertypes.RestartPolicyUnlessStopped)
413
+		runTest(t, containertypes.RestartPolicyOnFailure)
414
+		runTest(t, containertypes.RestartPolicyDisabled)
415 415
 	})
416 416
 
417 417
 	// Make sure that the local volume driver's mount ref count is restored
... ...
@@ -169,7 +169,7 @@ func WithPidsLimit(limit *int64) func(*TestContainerConfig) {
169 169
 }
170 170
 
171 171
 // WithRestartPolicy sets container's restart policy
172
-func WithRestartPolicy(policy string) func(c *TestContainerConfig) {
172
+func WithRestartPolicy(policy container.RestartPolicyMode) func(c *TestContainerConfig) {
173 173
 	return func(c *TestContainerConfig) {
174 174
 		c.HostConfig.RestartPolicy.Name = policy
175 175
 	}