diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 9a20db1532..5407a0114a 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 82c5ab665d..3d9e471409 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 3abd8946d7..e9f4183aee 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 b9afa7ef65..2faaefbf65 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 75ea835bd7..88f6778092 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 e2fdcddd96..9649d03070 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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 `, }, diff --git a/cli/compose/schema/data/config_schema_v3.7.json b/cli/compose/schema/data/config_schema_v3.7.json index f85efe34f3..4c3d24dcd2 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 f80af40113..c029f2f725 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 107dc85784..397bc79c1f 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