Repository: compose-spec/compose-go
Pull request #428 (2023-07-04 15:58:36)

add support of required attribute of depends_on
Implementation of the [following spec proposal](https://github.com/compose-spec/compose-spec/pull/382)

glours: Check of schema validation won't pass until the Spec PR will be merged.

Patch:
diff --git a/loader/loader.go b/loader/loader.go
index 907d633..76dbe2f 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -1088,13 +1088,24 @@ var transformDependsOnConfig TransformerFunc = func(data interface{}) (interface
 		for _, serviceIntf := range value {
 			service, ok := serviceIntf.(string)
 			if !ok {
-				return data, errors.Errorf("invalid type %T for service depends_on elementn, expected string", value)
+				return data, errors.Errorf("invalid type %T for service depends_on element, expected string", value)
 			}
-			transformed[service] = map[string]interface{}{"condition": types.ServiceConditionStarted}
+			transformed[service] = map[string]interface{}{"condition": types.ServiceConditionStarted, "required": true}
 		}
 		return transformed, nil
 	case map[string]interface{}:
-		return groupXFieldsIntoExtensions(data.(map[string]interface{})), nil
+		transformed := map[string]interface{}{}
+		for service, val := range value {
+			dependsConfigIntf, ok := val.(map[string]interface{})
+			if !ok {
+				return data, errors.Errorf("invalid type %T for service depends_on element", value)
+			}
+			if _, ok := dependsConfigIntf["required"]; !ok {
+				dependsConfigIntf["required"] = true
+			}
+			transformed[service] = dependsConfigIntf
+		}
+		return groupXFieldsIntoExtensions(transformed), nil
 	default:
 		return data, errors.Errorf("invalid type %T for service depends_on", value)
 	}
diff --git a/loader/normalize.go b/loader/normalize.go
index 1fe9fba..df7812a 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -83,6 +83,7 @@ func Normalize(project *types.Project) error {
 			s.DependsOn = setIfMissing(s.DependsOn, link, types.ServiceDependency{
 				Condition: types.ServiceConditionStarted,
 				Restart:   true,
+				Required:  true,
 			})
 		}
 
@@ -92,6 +93,7 @@ func Normalize(project *types.Project) error {
 				s.DependsOn = setIfMissing(s.DependsOn, name, types.ServiceDependency{
 					Condition: types.ServiceConditionStarted,
 					Restart:   true,
+					Required:  true,
 				})
 			}
 		}
@@ -102,6 +104,7 @@ func Normalize(project *types.Project) error {
 				s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{
 					Condition: types.ServiceConditionStarted,
 					Restart:   false,
+					Required:  true,
 				})
 			}
 		}
@@ -194,6 +197,7 @@ func inferImplicitDependencies(service *types.ServiceConfig) {
 		if _, ok := service.DependsOn[d]; !ok {
 			service.DependsOn[d] = types.ServiceDependency{
 				Condition: types.ServiceConditionStarted,
+				Required:  true,
 			}
 		}
 	}
diff --git a/types/types.go b/types/types.go
index 18d8fe7..f904ce8 100644
--- a/types/types.go
+++ b/types/types.go
@@ -1025,6 +1025,7 @@ type ServiceDependency struct {
 	Condition  string     `yaml:"condition,omitempty" json:"condition,omitempty"`
 	Restart    bool       `yaml:"restart,omitempty" json:"restart,omitempty"`
 	Extensions Extensions `yaml:"#extensions,inline" json:"-"`
+	Required   bool       `yaml:"required" json:"required"`
 }
 
 type ExtendsConfig struct {


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index aa7a41c..513e245 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -95,8 +95,8 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
 			},
 			ContainerName: "my-web-container",
 			DependsOn: types.DependsOnConfig{
-				"db":    {Condition: types.ServiceConditionStarted},
-				"redis": {Condition: types.ServiceConditionStarted},
+				"db":    {Condition: types.ServiceConditionStarted, Required: true},
+				"redis": {Condition: types.ServiceConditionStarted, Required: true},
 			},
 			Deploy: &types.DeployConfig{
 				Mode:     "replicated",
@@ -657,8 +657,10 @@ services:
     depends_on:
       db:
         condition: service_started
+        required: true
       redis:
         condition: service_started
+        required: true
     deploy:
       mode: replicated
       replicas: 6
@@ -1228,10 +1230,12 @@ func fullExampleJSON(workingDir, homeDir string) string {
       "container_name": "my-web-container",
       "depends_on": {
         "db": {
-          "condition": "service_started"
+          "condition": "service_started",
+          "required": true
         },
         "redis": {
-          "condition": "service_started"
+          "condition": "service_started",
+          "required": true
         }
       },
       "deploy": {
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 8d15202..b8b889a 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2428,7 +2428,7 @@ services:
 			Image:       "busybox",
 			Environment: types.MappingWithEquals{},
 			Scale:       1,
-			DependsOn:   types.DependsOnConfig{"imported": {Condition: "service_started"}},
+			DependsOn:   types.DependsOnConfig{"imported": {Condition: "service_started", Required: true}},
 		},
 		{
 			Name:          "imported",
@@ -2465,3 +2465,35 @@ func TestLoadWithIncludeCycle(t *testing.T) {
 	})
 	assert.Check(t, strings.HasPrefix(err.Error(), "include cycle detected"))
 }
+
+func TestLoadWithDependsOn(t *testing.T) {
+	p, err := loadYAML(`
+name: test-depends-on
+services:
+  foo:
+    image: nginx
+    depends_on:
+      bar:
+        condition: service_started
+      baz:
+        condition: service_healthy
+        required: false
+      qux:
+        condition: service_completed_successfully
+        required: true
+`)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services, types.Services{
+		{
+			Name:        "foo",
+			Image:       "nginx",
+			Environment: types.MappingWithEquals{},
+			Scale:       1,
+			DependsOn: types.DependsOnConfig{
+				"bar": {Condition: types.ServiceConditionStarted, Required: true},
+				"baz": {Condition: types.ServiceConditionHealthy, Required: false},
+				"qux": {Condition: types.ServiceConditionCompletedSuccessfully, Required: true},
+			},
+		},
+	})
+}
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index e68f13c..db8eca3 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -136,6 +136,7 @@ func TestNormalizeDependsOn(t *testing.T) {
 					"bar": { // explicit depends_on never should be overridden
 						Condition: types.ServiceConditionHealthy,
 						Restart:   false,
+						Required:  true,
 					},
 				},
 				NetworkMode: "service:zot",
@@ -159,6 +160,7 @@ services:
     depends_on:
       zot:
         condition: service_started
+        required: true
     networks:
       default: null
     volumes_from:
@@ -168,9 +170,11 @@ services:
     depends_on:
       bar:
         condition: service_healthy
+        required: true
       zot:
         condition: service_started
         restart: true
+        required: true
     network_mode: service:zot
   zot:
     networks:
@@ -200,19 +204,19 @@ func TestNormalizeImplicitDependencies(t *testing.T) {
 				Links:       []string{"corge"},
 				DependsOn: map[string]types.ServiceDependency{
 					// explicit dependency MUST not be overridden
-					"foo": {Condition: types.ServiceConditionHealthy, Restart: false},
+					"foo": {Condition: types.ServiceConditionHealthy, Restart: false, Required: true},
 				},
 			},
 		},
 	}
 
 	expected := types.DependsOnConfig{
-		"foo":   {Condition: types.ServiceConditionHealthy, Restart: false},
-		"bar":   {Condition: types.ServiceConditionStarted, Restart: true},
-		"baz":   {Condition: types.ServiceConditionStarted, Restart: true},
-		"qux":   {Condition: types.ServiceConditionStarted, Restart: true},
-		"quux":  {Condition: types.ServiceConditionStarted},
-		"corge": {Condition: types.ServiceConditionStarted, Restart: true},
+		"foo":   {Condition: types.ServiceConditionHealthy, Restart: false, Required: true},
+		"bar":   {Condition: types.ServiceConditionStarted, Restart: true, Required: true},
+		"baz":   {Condition: types.ServiceConditionStarted, Restart: true, Required: true},
+		"qux":   {Condition: types.ServiceConditionStarted, Restart: true, Required: true},
+		"quux":  {Condition: types.ServiceConditionStarted, Required: true},
+		"corge": {Condition: types.ServiceConditionStarted, Restart: true, Required: true},
 	}
 	err := Normalize(&project)
 	assert.NilError(t, err)