Skip to content

Commit f1c3df1

Browse files
committed
refactor parsing restart-policies
- opts.ParseRestartPolicy: use a struct-literal and clarify that this function only parses, but does not validate invalid combinations. - cli/compose/convert: convertRestartPolicy: update doc to be more clear on the order of preference and intent. - cli/compose/convert: convertRestartPolicy: use a switch based on known values to allow the exhaustive linter to catch missing options. - cli/compose/convert: convertRestartPolicy: add validation for negative values when converting the legacy service.restart policy. - cli/compose/convert: convertRestartPolicy: always set MaxAttempts and leave validation to the daemon. opts: ParseRestartPolicy: improve validation of max restart-counts Use the new container.ValidateRestartPolicy utility to verify if a max-restart-count is allowed for the given restart-policy. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 055a478 commit f1c3df1

4 files changed

Lines changed: 79 additions & 50 deletions

File tree

cli/command/container/opts_test.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -829,13 +829,6 @@ func TestParseRestartPolicy(t *testing.T) {
829829
Name: container.RestartPolicyAlways,
830830
},
831831
},
832-
{
833-
input: "always:1",
834-
expected: container.RestartPolicy{
835-
Name: container.RestartPolicyAlways,
836-
MaximumRetryCount: 1,
837-
},
838-
},
839832
{
840833
input: "always:2:3",
841834
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
@@ -861,6 +854,16 @@ func TestParseRestartPolicy(t *testing.T) {
861854
input: "unless-stopped:invalid",
862855
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
863856
},
857+
858+
// Unknown / invalid combinations: validation is handled by the daemon>
859+
{
860+
input: "anything:123",
861+
expected: container.RestartPolicy{Name: "anything", MaximumRetryCount: 123},
862+
},
863+
{
864+
input: "negative:-123",
865+
expected: container.RestartPolicy{Name: "negative", MaximumRetryCount: -123},
866+
},
864867
}
865868
for _, tc := range tests {
866869
t.Run(tc.input, func(t *testing.T) {

cli/compose/convert/service.go

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ func Service(
8585
return swarm.ServiceSpec{}, err
8686
}
8787

88-
restartPolicy, err := convertRestartPolicy(
89-
service.Restart, service.Deploy.RestartPolicy)
88+
restartPolicy, err := convertRestartPolicy(service.Restart, service.Deploy.RestartPolicy)
9089
if err != nil {
9190
return swarm.ServiceSpec{}, err
9291
}
@@ -472,37 +471,56 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
472471
}, nil
473472
}
474473

475-
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
476-
// TODO: log if restart is being ignored
477-
if source == nil {
478-
policy, err := opts.ParseRestartPolicy(restart)
479-
if err != nil {
480-
return nil, err
481-
}
482-
switch {
483-
case policy.IsNone():
484-
return nil, nil
485-
case policy.IsAlways(), policy.IsUnlessStopped():
486-
return &swarm.RestartPolicy{
487-
Condition: swarm.RestartPolicyConditionAny,
488-
}, nil
489-
case policy.IsOnFailure():
490-
attempts := uint64(policy.MaximumRetryCount)
491-
return &swarm.RestartPolicy{
492-
Condition: swarm.RestartPolicyConditionOnFailure,
493-
MaxAttempts: &attempts,
494-
}, nil
495-
default:
496-
return nil, fmt.Errorf("unknown restart policy: %s", restart)
474+
// convertRestartPolicy converts the service's restart-policy. It prefers
475+
// service.deploy.restart_policy, but falls back to parsing the legacy
476+
// service.restart if service.deploy.restart_policy is not set.
477+
func convertRestartPolicy(restart string, restartPolicy *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
478+
// Use service.deploy.restart_policy, if set.
479+
if restartPolicy != nil {
480+
// TODO: log or error if both "service.restart" and "service.deploy.restartpolicy" are set.
481+
return &swarm.RestartPolicy{
482+
Condition: swarm.RestartPolicyCondition(restartPolicy.Condition),
483+
Delay: composetypes.ConvertDurationPtr(restartPolicy.Delay),
484+
MaxAttempts: restartPolicy.MaxAttempts,
485+
Window: composetypes.ConvertDurationPtr(restartPolicy.Window),
486+
}, nil
487+
}
488+
if restart == "" {
489+
return nil, nil
490+
}
491+
492+
// Fall back to the legacy service.restart restart-policy.
493+
policy, err := opts.ParseRestartPolicy(restart)
494+
if err != nil {
495+
return nil, err
496+
}
497+
if policy.MaximumRetryCount < 0 {
498+
return nil, errors.New("invalid restart policy: maximum retry count cannot be negative")
499+
}
500+
uint64Ptr := func(i int) *uint64 {
501+
if i <= 0 {
502+
return nil
497503
}
504+
p := uint64(i)
505+
return &p
498506
}
499507

500-
return &swarm.RestartPolicy{
501-
Condition: swarm.RestartPolicyCondition(source.Condition),
502-
Delay: composetypes.ConvertDurationPtr(source.Delay),
503-
MaxAttempts: source.MaxAttempts,
504-
Window: composetypes.ConvertDurationPtr(source.Window),
505-
}, nil
508+
switch policy.Name {
509+
case container.RestartPolicyDisabled, "":
510+
return nil, nil
511+
case container.RestartPolicyAlways, container.RestartPolicyUnlessStopped:
512+
return &swarm.RestartPolicy{
513+
Condition: swarm.RestartPolicyConditionAny,
514+
MaxAttempts: uint64Ptr(policy.MaximumRetryCount),
515+
}, nil
516+
case container.RestartPolicyOnFailure:
517+
return &swarm.RestartPolicy{
518+
Condition: swarm.RestartPolicyConditionOnFailure,
519+
MaxAttempts: uint64Ptr(policy.MaximumRetryCount),
520+
}, nil
521+
default:
522+
return nil, fmt.Errorf("invalid restart policy: unknown policy '%s'", restart)
523+
}
506524
}
507525

508526
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {

cli/compose/convert/service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestConvertRestartPolicyFromNone(t *testing.T) {
2626

2727
func TestConvertRestartPolicyFromUnknown(t *testing.T) {
2828
_, err := convertRestartPolicy("unknown", nil)
29-
assert.Error(t, err, "unknown restart policy: unknown")
29+
assert.Error(t, err, "invalid restart policy: unknown policy 'unknown'; use one of 'no', 'always', 'on-failure', or 'unless-stopped'")
3030
}
3131

3232
func TestConvertRestartPolicyFromAlways(t *testing.T) {

opts/parse.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,36 @@ func ConvertKVStringsToMapWithNil(values []string) map[string]*string {
7070
return result
7171
}
7272

73-
// ParseRestartPolicy returns the parsed policy or an error indicating what is incorrect
73+
// ParseRestartPolicy parses a restart policy string ("name[:max-retries]")
74+
// into a [container.RestartPolicy].
75+
//
76+
// Parsing is syntactic only; semantic validation is deferred to the daemon/API.
77+
// An empty input returns a zero-value policy for backward compatibility. The
78+
// retry count, if set, must be an integer (negative values are allowed here
79+
// but may be rejected by the daemon).
7480
func ParseRestartPolicy(policy string) (container.RestartPolicy, error) {
7581
if policy == "" {
76-
// for backward-compatibility, we don't set the default ("no")
77-
// policy here, because older versions of the engine may not
78-
// support it.
82+
// For backward compatibility, do not set an explicit default ("no"),
83+
// as older daemons may not support it.
7984
return container.RestartPolicy{}, nil
8085
}
8186

82-
p := container.RestartPolicy{}
83-
k, v, ok := strings.Cut(policy, ":")
84-
if ok && k == "" {
87+
name, count, ok := strings.Cut(policy, ":")
88+
if ok && name == "" {
8589
return container.RestartPolicy{}, errors.New("invalid restart policy format: no policy provided before colon")
8690
}
87-
if v != "" {
88-
count, err := strconv.Atoi(v)
91+
92+
var retryCount int
93+
if count != "" {
94+
c, err := strconv.Atoi(count)
8995
if err != nil {
9096
return container.RestartPolicy{}, errors.New("invalid restart policy format: maximum retry count must be an integer")
9197
}
92-
p.MaximumRetryCount = count
98+
retryCount = c
9399
}
94100

95-
p.Name = container.RestartPolicyMode(k)
96-
return p, nil
101+
return container.RestartPolicy{
102+
Name: container.RestartPolicyMode(name),
103+
MaximumRetryCount: retryCount,
104+
}, nil
97105
}

0 commit comments

Comments
 (0)