Merge pull request #4535 from thaJeztah/restart_policy_more_validate

refactor parsing restart-policies
This commit is contained in:
Paweł Gronowski
2026-05-11 13:57:33 +02:00
committed by GitHub
4 changed files with 125 additions and 77 deletions
+10 -7
View File
@@ -829,13 +829,6 @@ func TestParseRestartPolicy(t *testing.T) {
Name: container.RestartPolicyAlways,
},
},
{
input: "always:1",
expected: container.RestartPolicy{
Name: container.RestartPolicyAlways,
MaximumRetryCount: 1,
},
},
{
input: "always:2:3",
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
@@ -861,6 +854,16 @@ func TestParseRestartPolicy(t *testing.T) {
input: "unless-stopped:invalid",
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
},
// Unknown / invalid combinations: validation is handled by the daemon>
{
input: "anything:123",
expected: container.RestartPolicy{Name: "anything", MaximumRetryCount: 123},
},
{
input: "negative:-123",
expected: container.RestartPolicy{Name: "negative", MaximumRetryCount: -123},
},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
+51 -31
View File
@@ -85,8 +85,7 @@ func Service(
return swarm.ServiceSpec{}, err
}
restartPolicy, err := convertRestartPolicy(
service.Restart, service.Deploy.RestartPolicy)
restartPolicy, err := convertRestartPolicy(service.Restart, service.Deploy.RestartPolicy)
if err != nil {
return swarm.ServiceSpec{}, err
}
@@ -472,37 +471,58 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
}, nil
}
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
// TODO: log if restart is being ignored
if source == nil {
policy, err := opts.ParseRestartPolicy(restart)
if err != nil {
return nil, err
}
switch {
case policy.IsNone():
return nil, nil
case policy.IsAlways(), policy.IsUnlessStopped():
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
}, nil
case policy.IsOnFailure():
attempts := uint64(policy.MaximumRetryCount)
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
}, nil
default:
return nil, fmt.Errorf("unknown restart policy: %s", restart)
}
// convertRestartPolicy converts the service's restart-policy. It prefers
// service.deploy.restart_policy, but falls back to parsing the legacy
// service.restart if service.deploy.restart_policy is not set.
func convertRestartPolicy(restart string, restartPolicy *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
// Use service.deploy.restart_policy, if set.
if restartPolicy != nil {
// TODO: log or error if both "service.restart" and "service.deploy.restartpolicy" are set.
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyCondition(restartPolicy.Condition),
Delay: composetypes.ConvertDurationPtr(restartPolicy.Delay),
MaxAttempts: restartPolicy.MaxAttempts,
Window: composetypes.ConvertDurationPtr(restartPolicy.Window),
}, nil
}
if restart == "" {
return nil, nil
}
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyCondition(source.Condition),
Delay: composetypes.ConvertDurationPtr(source.Delay),
MaxAttempts: source.MaxAttempts,
Window: composetypes.ConvertDurationPtr(source.Window),
}, nil
// Fall back to the legacy service.restart restart-policy.
policy, err := opts.ParseRestartPolicy(restart)
if err != nil {
return nil, err
}
if policy.MaximumRetryCount < 0 {
return nil, errors.New("invalid restart policy: maximum retry count cannot be negative")
}
uint64Ptr := func(i int) *uint64 {
if i <= 0 {
return nil
}
p := uint64(i)
return &p
}
switch policy.Name {
case container.RestartPolicyDisabled, "":
return nil, nil
case container.RestartPolicyAlways, container.RestartPolicyUnlessStopped:
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
MaxAttempts: uint64Ptr(policy.MaximumRetryCount),
}, nil
case container.RestartPolicyOnFailure:
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: uint64Ptr(policy.MaximumRetryCount),
}, nil
default:
return nil, fmt.Errorf("invalid restart policy: unknown policy '%s' (must be one of '%s', '%s', '%s', or '%s')",
policy.Name, container.RestartPolicyDisabled, container.RestartPolicyAlways, container.RestartPolicyOnFailure, container.RestartPolicyUnlessStopped,
)
}
}
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
+44 -27
View File
@@ -18,35 +18,52 @@ import (
is "gotest.tools/v3/assert/cmp"
)
func TestConvertRestartPolicyFromNone(t *testing.T) {
policy, err := convertRestartPolicy("no", nil)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy))
}
func TestConvertRestartPolicyFromUnknown(t *testing.T) {
_, err := convertRestartPolicy("unknown", nil)
assert.Error(t, err, "unknown restart policy: unknown")
}
func TestConvertRestartPolicyFromAlways(t *testing.T) {
policy, err := convertRestartPolicy("always", nil)
expected := &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, policy))
}
func TestConvertRestartPolicyFromFailure(t *testing.T) {
policy, err := convertRestartPolicy("on-failure:4", nil)
func TestConvertRestartPolicy(t *testing.T) {
attempts := uint64(4)
expected := &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
tests := []struct {
input string
expected *swarm.RestartPolicy
expError string
}{
{},
{
input: "no",
},
{
input: "unknown",
expError: "invalid restart policy: unknown policy 'unknown'",
},
{
input: "always",
expected: &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
},
},
{
input: "on-failure:4",
expected: &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
},
},
}
for _, tc := range tests {
name := tc.input
if name == "" {
name = "empty"
}
t.Run(name, func(t *testing.T) {
policy, err := convertRestartPolicy(tc.input, nil)
if tc.expError != "" {
assert.Check(t, is.ErrorContains(err, tc.expError))
assert.Check(t, is.Nil(policy))
return
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(policy, tc.expected))
})
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, policy))
}
func strPtr(val string) *string {
+20 -12
View File
@@ -70,28 +70,36 @@ func ConvertKVStringsToMapWithNil(values []string) map[string]*string {
return result
}
// ParseRestartPolicy returns the parsed policy or an error indicating what is incorrect
// ParseRestartPolicy parses a restart policy string ("name[:max-retries]")
// into a [container.RestartPolicy].
//
// Parsing is syntactic only; semantic validation is deferred to the daemon/API.
// An empty input returns a zero-value policy for backward compatibility. The
// retry count, if set, must be an integer (negative values are allowed here
// but may be rejected by the daemon).
func ParseRestartPolicy(policy string) (container.RestartPolicy, error) {
if policy == "" {
// for backward-compatibility, we don't set the default ("no")
// policy here, because older versions of the engine may not
// support it.
// For backward compatibility, do not set an explicit default ("no"),
// as older daemons may not support it.
return container.RestartPolicy{}, nil
}
p := container.RestartPolicy{}
k, v, ok := strings.Cut(policy, ":")
if ok && k == "" {
name, count, ok := strings.Cut(policy, ":")
if ok && name == "" {
return container.RestartPolicy{}, errors.New("invalid restart policy format: no policy provided before colon")
}
if v != "" {
count, err := strconv.Atoi(v)
var retryCount int
if count != "" {
c, err := strconv.Atoi(count)
if err != nil {
return container.RestartPolicy{}, errors.New("invalid restart policy format: maximum retry count must be an integer")
}
p.MaximumRetryCount = count
retryCount = c
}
p.Name = container.RestartPolicyMode(k)
return p, nil
return container.RestartPolicy{
Name: container.RestartPolicyMode(name),
MaximumRetryCount: retryCount,
}, nil
}