diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 9a20db15324f..5407a0114ae7 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -159,9 +159,10 @@ func Service( Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), }, }, - EndpointSpec: endpoint, - Mode: mode, - UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), + EndpointSpec: endpoint, + Mode: mode, + UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), + RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig), } // add an image label to serviceSpec diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index 82c5ab665d29..3d9e47140972 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -550,3 +550,17 @@ func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOpt } return []swarm.Config{}, nil } + +func TestConvertUpdateConfigParallelism(t *testing.T) { + parallel := uint64(4) + + // test default behavior + updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) + assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism)) + + // Non default value + updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ + Parallelism: ¶llel, + }) + assert.Check(t, is.Equal(parallel, updateConfig.Parallelism)) +} diff --git a/cli/compose/loader/full-example.yml b/cli/compose/loader/full-example.yml index 3abd8946d7de..e9f4183aee28 100644 --- a/cli/compose/loader/full-example.yml +++ b/cli/compose/loader/full-example.yml @@ -1,4 +1,4 @@ -version: "3.6" +version: "3.7" services: foo: @@ -39,6 +39,13 @@ services: mode: replicated replicas: 6 labels: [FOO=BAR] + rollback_config: + parallelism: 3 + delay: 10s + failure_action: continue + monitor: 60s + max_failure_ratio: 0.3 + order: start-first update_config: parallelism: 3 delay: 10s diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index b9afa7ef65e1..2faaefbf65c1 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -10,7 +10,7 @@ import ( func fullExampleConfig(workingDir, homeDir string) *types.Config { return &types.Config{ - Version: "3.6", + Version: "3.7", Services: services(workingDir, homeDir), Networks: networks(), Volumes: volumes(), @@ -41,6 +41,14 @@ func services(workingDir, homeDir string) []types.ServiceConfig { Mode: "replicated", Replicas: uint64Ptr(6), Labels: map[string]string{"FOO": "BAR"}, + RollbackConfig: &types.UpdateConfig{ + Parallelism: uint64Ptr(3), + Delay: time.Duration(10 * time.Second), + FailureAction: "continue", + Monitor: time.Duration(60 * time.Second), + MaxFailureRatio: 0.3, + Order: "start-first", + }, UpdateConfig: &types.UpdateConfig{ Parallelism: uint64Ptr(3), Delay: time.Duration(10 * time.Second), @@ -393,7 +401,7 @@ func volumes() map[string]types.VolumeConfig { } func fullExampleYAML(workingDir string) string { - return fmt.Sprintf(`version: "3.6" + return fmt.Sprintf(`version: "3.7" services: foo: build: @@ -436,6 +444,13 @@ services: monitor: 1m0s max_failure_ratio: 0.3 order: start-first + rollback_config: + parallelism: 3 + delay: 10s + failure_action: continue + monitor: 1m0s + max_failure_ratio: 0.3 + order: start-first resources: limits: cpus: "0.001" diff --git a/cli/compose/loader/types_test.go b/cli/compose/loader/types_test.go index 75ea835bd789..88f677809211 100644 --- a/cli/compose/loader/types_test.go +++ b/cli/compose/loader/types_test.go @@ -19,7 +19,7 @@ func TestMarshallConfig(t *testing.T) { assert.Check(t, is.Equal(expected, string(actual))) // Make sure the expected still - dict, err := ParseYAML([]byte("version: '3.6'\n" + expected)) + dict, err := ParseYAML([]byte("version: '3.7'\n" + expected)) assert.NilError(t, err) _, err = Load(buildConfigDetails(dict, map[string]string{})) assert.NilError(t, err) diff --git a/cli/compose/schema/bindata.go b/cli/compose/schema/bindata.go index 5e01c6df331a..fa6466a07005 100644 --- a/cli/compose/schema/bindata.go +++ b/cli/compose/schema/bindata.go @@ -467,44 +467,44 @@ DoTuq9lAU9Q4O1xV/59X/w8AAP//zRo7vm9CAAA= "/data/config_schema_v3.7.json": { local: "data/config_schema_v3.7.json", - size: 17007, + size: 17540, modtime: 1518458244, compressed: ` -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 -DoTuq9lAU9Q4O1xV/59X/w8AAP//zRo7vm9CAAA= +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 `, }, diff --git a/cli/compose/schema/data/config_schema_v3.7.json b/cli/compose/schema/data/config_schema_v3.7.json index 95a552b346cb..db5195aeab20 100644 --- a/cli/compose/schema/data/config_schema_v3.7.json +++ b/cli/compose/schema/data/config_schema_v3.7.json @@ -348,6 +348,20 @@ "endpoint_mode": {"type": "string"}, "replicas": {"type": "integer"}, "labels": {"$ref": "#/definitions/list_or_dict"}, + "rollback_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"}, + "order": {"type": "string", "enum": [ + "start-first", "stop-first" + ]} + }, + "additionalProperties": false + }, "update_config": { "type": "object", "properties": { diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index f80af4011359..c029f2f7259f 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -114,3 +114,92 @@ func TestValidateIsolation(t *testing.T) { } assert.NilError(t, Validate(config, "3.5")) } + +func TestValidateRollbackConfig(t *testing.T) { + config := dict{ + "version": "3.4", + "services": dict{ + "foo": dict{ + "image": "busybox", + "deploy": dict{ + "rollback_config": dict{ + "parallelism": 1, + }, + }, + }, + }, + } + + assert.NilError(t, Validate(config, "3.7")) +} + +func TestValidateRollbackConfigWithOrder(t *testing.T) { + config := dict{ + "version": "3.4", + "services": dict{ + "foo": dict{ + "image": "busybox", + "deploy": dict{ + "rollback_config": dict{ + "parallelism": 1, + "order": "start-first", + }, + }, + }, + }, + } + + assert.NilError(t, Validate(config, "3.7")) +} + +func TestValidateRollbackConfigWithUpdateConfig(t *testing.T) { + config := dict{ + "version": "3.4", + "services": dict{ + "foo": dict{ + "image": "busybox", + "deploy": dict{ + "update_config": dict{ + "parallelism": 1, + "order": "start-first", + }, + "rollback_config": dict{ + "parallelism": 1, + "order": "start-first", + }, + }, + }, + }, + } + + assert.NilError(t, Validate(config, "3.7")) +} + +func TestValidateRollbackConfigWithUpdateConfigFull(t *testing.T) { + config := dict{ + "version": "3.4", + "services": dict{ + "foo": dict{ + "image": "busybox", + "deploy": dict{ + "update_config": dict{ + "parallelism": 1, + "order": "start-first", + "delay": "10s", + "failure_action": "pause", + "monitor": "10s", + }, + "rollback_config": dict{ + "parallelism": 1, + "order": "start-first", + "delay": "10s", + "failure_action": "pause", + "monitor": "10s", + }, + }, + }, + }, + } + + assert.NilError(t, Validate(config, "3.7")) +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 107dc8578437..397bc79c1f04 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -190,14 +190,15 @@ type LoggingConfig struct { // DeployConfig the deployment configuration for a service type DeployConfig struct { - Mode string `yaml:",omitempty"` - Replicas *uint64 `yaml:",omitempty"` - Labels Labels `yaml:",omitempty"` - UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty"` - Resources Resources `yaml:",omitempty"` - RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty"` - Placement Placement `yaml:",omitempty"` - EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty"` + Mode string `yaml:",omitempty"` + Replicas *uint64 `yaml:",omitempty"` + Labels Labels `yaml:",omitempty"` + UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty"` + RollbackConfig *UpdateConfig `mapstructure:"rollback_config" yaml:"rollback_config,omitempty"` + Resources Resources `yaml:",omitempty"` + RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty"` + Placement Placement `yaml:",omitempty"` + EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty"` } // HealthCheckConfig the healthcheck configuration for a service