Repository: compose-spec/compose-go
Pull request #350 (2023-02-15 17:44:52)

introduce `restart` for depends_on
- introduces `restart` requirement for `depends_on`
- normalize all implicit dependencies between services (shared namespace, volumes from ...) into explicit depends_on relations

This allows to select dependencies to be considered depending on the context, vs just walk the dependency graph
see https://github.com/docker/compose/pull/10284 for usage

see https://github.com/compose-spec/compose-spec/pull/305


Patch:
diff --git a/loader/loader.go b/loader/loader.go
index d8ff2ef..93b3c93 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -233,7 +233,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 	}
 
 	if !opts.SkipNormalization {
-		err = normalize(project, opts.ResolvePaths)
+		err = Normalize(project, opts.ResolvePaths)
 		if err != nil {
 			return nil, err
 		}
diff --git a/loader/normalize.go b/loader/normalize.go
index c3aa91d..9153b56 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
@@ -27,8 +28,8 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
-func normalize(project *types.Project, resolvePaths bool) error {
+// Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
+func Normalize(project *types.Project, resolvePaths bool) error {
 	absWorkingDir, err := filepath.Abs(project.WorkingDir)
 	if err != nil {
 		return err
@@ -95,6 +96,37 @@ func normalize(project *types.Project, resolvePaths bool) error {
 			s.Extends["file"] = &p
 		}
 
+		for _, link := range s.Links {
+			parts := strings.Split(link, ":")
+			if len(parts) == 2 {
+				link = parts[0]
+			}
+			s.DependsOn = setIfMissing(s.DependsOn, link, types.ServiceDependency{
+				Condition: types.ServiceConditionStarted,
+				Restart:   true,
+			})
+		}
+
+		for _, namespace := range []string{s.NetworkMode, s.Ipc, s.Pid, s.Uts, s.Cgroup} {
+			if strings.HasPrefix(namespace, types.ServicePrefix) {
+				name := namespace[len(types.ServicePrefix):]
+				s.DependsOn = setIfMissing(s.DependsOn, name, types.ServiceDependency{
+					Condition: types.ServiceConditionStarted,
+					Restart:   true,
+				})
+			}
+		}
+
+		for _, vol := range s.VolumesFrom {
+			if !strings.HasPrefix(s.Pid, types.ContainerPrefix) {
+				spec := strings.Split(vol, ":")
+				s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{
+					Condition: types.ServiceConditionStarted,
+					Restart:   false,
+				})
+			}
+		}
+
 		err := relocateLogDriver(&s)
 		if err != nil {
 			return err
@@ -131,9 +163,20 @@ func normalize(project *types.Project, resolvePaths bool) error {
 	return nil
 }
 
+// setIfMissing adds a ServiceDependency for service if not already defined
+func setIfMissing(d types.DependsOnConfig, service string, dep types.ServiceDependency) types.DependsOnConfig {
+	if d == nil {
+		d = types.DependsOnConfig{}
+	}
+	if _, ok := d[service]; !ok {
+		d[service] = dep
+	}
+	return d
+}
+
 func relocateScale(s *types.ServiceConfig) error {
 	scale := uint64(s.Scale)
-	if scale != 1 {
+	if scale > 1 {
 		logrus.Warn("`scale` is deprecated. Use the `deploy.replicas` element")
 		if s.Deploy == nil {
 			s.Deploy = &types.DeployConfig{}
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 6f82797..ac3eafb 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -181,6 +181,7 @@
                   "type": "object",
                   "additionalProperties": false,
                   "properties": {
+                    "restart": {"type": "boolean"},
                     "condition": {
                       "type": "string",
                       "enum": ["service_started", "service_healthy", "service_completed_successfully"]
diff --git a/types/types.go b/types/types.go
index e920e63..9c6b322 100644
--- a/types/types.go
+++ b/types/types.go
@@ -256,35 +256,11 @@ const (
 
 // GetDependencies retrieve all services this service depends on
 func (s ServiceConfig) GetDependencies() []string {
-	dependencies := make(set)
-	for dependency := range s.DependsOn {
-		dependencies.append(dependency)
-	}
-	for _, link := range s.Links {
-		parts := strings.Split(link, ":")
-		if len(parts) == 2 {
-			dependencies.append(parts[0])
-		} else {
-			dependencies.append(link)
-		}
-	}
-	if strings.HasPrefix(s.NetworkMode, ServicePrefix) {
-		dependencies.append(s.NetworkMode[len(ServicePrefix):])
-	}
-	if strings.HasPrefix(s.Ipc, ServicePrefix) {
-		dependencies.append(s.Ipc[len(ServicePrefix):])
+	var dependencies []string
+	for service := range s.DependsOn {
+		dependencies = append(dependencies, service)
 	}
-	if strings.HasPrefix(s.Pid, ServicePrefix) {
-		dependencies.append(s.Pid[len(ServicePrefix):])
-	}
-	for _, vol := range s.VolumesFrom {
-		if !strings.HasPrefix(s.Pid, ContainerPrefix) {
-			spec := strings.Split(vol, ":")
-			dependencies.append(spec[0])
-		}
-	}
-
-	return dependencies.toSlice()
+	return dependencies
 }
 
 type set map[string]struct{}
@@ -1007,6 +983,7 @@ type DependsOnConfig map[string]ServiceDependency
 
 type ServiceDependency struct {
 	Condition  string                 `yaml:",omitempty" json:"condition,omitempty"`
+	Restart    bool                   `yaml:",omitempty" json:"restart,omitempty"`
 	Extensions map[string]interface{} `yaml:",inline" json:"-"`
 }
 


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index a548f33..bae478f 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -783,7 +783,7 @@ services:
     build:
      context: ./web
     links:
-      - bar
+      - db
     pid: host
   db:
     image: db
@@ -837,7 +837,7 @@ services:
     image: web
     build: .
     links:
-      - bar
+      - db
   db:
     image: db
     build:
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index c32a78e..93e8e71 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -82,7 +82,7 @@ networks:
   mynet:
     name: myProject_mynet
 `
-	err := normalize(&project, false)
+	err := Normalize(&project, false)
 	assert.NilError(t, err)
 	marshal, err := yaml.Marshal(project)
 	assert.NilError(t, err)
@@ -118,7 +118,7 @@ networks:
   default:
     name: myProject_default
 `, filepath.Join(wd, "testdata"))
-	err := normalize(&project, true)
+	err := Normalize(&project, true)
 	assert.NilError(t, err)
 	marshal, err := yaml.Marshal(project)
 	assert.NilError(t, err)
@@ -142,7 +142,7 @@ func TestNormalizeAbsolutePaths(t *testing.T) {
 		WorkingDir:   absWorkingDir,
 		ComposeFiles: []string{absComposeFile, absOverrideFile},
 	}
-	err := normalize(&project, false)
+	err := Normalize(&project, false)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expected, project)
 }
@@ -180,7 +180,65 @@ func TestNormalizeVolumes(t *testing.T) {
 		WorkingDir:   absCwd,
 		ComposeFiles: []string{},
 	}
-	err := normalize(&project, false)
+	err := Normalize(&project, false)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expected, project)
 }
+
+func TestNormalizeDependsOn(t *testing.T) {
+	project := types.Project{
+		Name:     "myProject",
+		Networks: types.Networks{},
+		Volumes:  types.Volumes{},
+		Services: []types.ServiceConfig{
+			{
+				Name: "foo",
+				DependsOn: map[string]types.ServiceDependency{
+					"bar": { // explicit depends_on never should be overridden
+						Condition: types.ServiceConditionHealthy,
+						Restart:   false,
+					},
+				},
+				NetworkMode: "service:zot",
+			},
+			{
+				Name:        "bar",
+				VolumesFrom: []string{"zot"},
+			},
+			{
+				Name: "zot",
+			},
+		},
+	}
+
+	expected := `name: myProject
+services:
+  bar:
+    depends_on:
+      zot:
+        condition: service_started
+    networks:
+      default: null
+    volumes_from:
+    - zot
+  foo:
+    depends_on:
+      bar:
+        condition: service_healthy
+      zot:
+        condition: service_started
+        restart: true
+    network_mode: service:zot
+  zot:
+    networks:
+      default: null
+networks:
+  default:
+    name: myProject_default
+`
+	err := Normalize(&project, true)
+	assert.NilError(t, err)
+	marshal, err := yaml.Marshal(project)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expected, string(marshal))
+}