Repository: compose-spec/compose-go
Pull request #109 (2021-02-18 02:55:30)

Validate network_mode for service and container
Note that it also refactors `Project::GetServices` and `Project::GetService`


Patch:
diff --git a/loader/validate.go b/loader/validate.go
index 18abb43..b853d17 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -18,6 +18,7 @@ package loader
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
@@ -36,6 +37,21 @@ func checkConsistency(project *types.Project) error {
 				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined network %s", s.Name, network))
 			}
 		}
+
+		if strings.HasPrefix(s.NetworkMode, types.NetworkModeServicePrefix) {
+			serviceName := s.NetworkMode[len(types.NetworkModeServicePrefix):]
+			if _, err := project.GetServices(serviceName); err != nil {
+				return fmt.Errorf("service %q not found for network_mode 'service:%s'", serviceName, serviceName)
+			}
+		}
+
+		if strings.HasPrefix(s.NetworkMode, types.NetworkModeContainerPrefix) {
+			containerName := s.NetworkMode[len(types.NetworkModeContainerPrefix):]
+			if _, err := project.GetByContainerName(containerName); err != nil {
+				return fmt.Errorf("service with container_name %q not found for network_mode 'container:%s'", containerName, containerName)
+			}
+		}
+
 		for _, volume := range s.Volumes {
 			switch volume.Type {
 			case types.VolumeTypeVolume:
diff --git a/types/project.go b/types/project.go
index cff7821..fd8495e 100644
--- a/types/project.go
+++ b/types/project.go
@@ -93,30 +93,56 @@ func (p Project) ConfigNames() []string {
 	return names
 }
 
+func (p Project) GetByContainerName(names ...string) (Services, error) {
+	if len(names) == 0 {
+		return p.Services, nil
+	}
+	services := Services{}
+outLoop:
+	for _, name := range names {
+		for _, s := range p.Services {
+			if name == s.ContainerName {
+				services = append(services, s)
+				continue outLoop
+			}
+		}
+		return nil, fmt.Errorf("service with container_name %q could not be found", name)
+	}
+	return services, nil
+}
+
 // GetServices retrieve services by names, or return all services if no name specified
-func (p Project) GetServices(names []string) (Services, error) {
+func (p Project) GetServices(names ...string) (Services, error) {
 	if len(names) == 0 {
 		return p.Services, nil
 	}
 	services := Services{}
 	for _, name := range names {
-		service, err := p.GetService(name)
-		if err != nil {
-			return nil, err
+		var serviceConfig *ServiceConfig
+		for _, s := range p.Services {
+			if s.Name == name {
+				serviceConfig = &s
+				break
+			}
+		}
+		if serviceConfig == nil {
+			return services, fmt.Errorf("no such service: %s", name)
 		}
-		services = append(services, service)
+		services = append(services, *serviceConfig)
 	}
 	return services, nil
 }
 
 // GetService retrieve a specific service by name
 func (p Project) GetService(name string) (ServiceConfig, error) {
-	for _, s := range p.Services {
-		if s.Name == name {
-			return s, nil
-		}
+	services, err := p.GetServices(name)
+	if err != nil {
+		return ServiceConfig{}, err
+	}
+	if len(services) == 0 {
+		return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
 	}
-	return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
+	return services[0], nil
 }
 
 func (p Project) AllServices() Services {
@@ -134,7 +160,7 @@ func (p Project) WithServices(names []string, fn ServiceFunc) error {
 }
 
 func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bool) error {
-	services, err := p.GetServices(names)
+	services, err := p.GetServices(names...)
 	if err != nil {
 		return err
 	}
diff --git a/types/types.go b/types/types.go
index 4a864bf..8bb7650 100644
--- a/types/types.go
+++ b/types/types.go
@@ -187,6 +187,13 @@ const (
 	PullPolicyBuild = "build"
 )
 
+const (
+	// NetworkModeServicePrefix is the prefix for network_mode pointing to a service
+	NetworkModeServicePrefix = "service:"
+	// NetworkModeContainerPrefix is the prefix for network_mode pointing to a container
+	NetworkModeContainerPrefix = "container:"
+)
+
 // GetDependencies retrieve all services this service depends on
 func (s ServiceConfig) GetDependencies() []string {
 	dependencies := make(set)
@@ -201,8 +208,8 @@ func (s ServiceConfig) GetDependencies() []string {
 			dependencies.append(link)
 		}
 	}
-	if strings.HasPrefix(s.NetworkMode, "service:") {
-		dependencies.append(s.NetworkMode[8:])
+	if strings.HasPrefix(s.NetworkMode, NetworkModeServicePrefix) {
+		dependencies.append(s.NetworkMode[len(NetworkModeServicePrefix):])
 	}
 	return dependencies.toSlice()
 }


Test patch:
diff --git a/loader/validate_test.go b/loader/validate_test.go
index 1eed167..3e9f295 100644
--- a/loader/validate_test.go
+++ b/loader/validate_test.go
@@ -19,8 +19,9 @@ package loader
 import (
 	"testing"
 
-	"github.com/compose-spec/compose-go/types"
 	"gotest.tools/v3/assert"
+
+	"github.com/compose-spec/compose-go/types"
 )
 
 func TestValidateAnonymousVolume(t *testing.T) {
@@ -81,3 +82,78 @@ func TestValidateNoBuildNoImage(t *testing.T) {
 	err := checkConsistency(project)
 	assert.Error(t, err, `service "myservice" has neither an image nor a build context specified: invalid compose project`)
 }
+
+func TestValidateNetworkMode(t *testing.T) {
+	t.Run("network_mode service fail", func(t *testing.T) {
+		project := &types.Project{
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:  "myservice1",
+					Image: "scratch",
+				},
+				{
+					Name:        "myservice2",
+					Image:       "scratch",
+					NetworkMode: "service:myservice1",
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+
+	t.Run("network_mode service fail", func(t *testing.T) {
+		project := &types.Project{
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:  "myservice1",
+					Image: "scratch",
+				},
+				{
+					Name:        "myservice2",
+					Image:       "scratch",
+					NetworkMode: "service:nonexistentservice",
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.Error(t, err, `service "nonexistentservice" not found for network_mode 'service:nonexistentservice'`)
+	})
+
+	t.Run("network_mode container", func(t *testing.T) {
+		project := &types.Project{
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:          "myservice1",
+					ContainerName: "mycontainer_name",
+					Image:         "scratch",
+				},
+				{
+					Name:        "myservice2",
+					Image:       "scratch",
+					NetworkMode: "container:mycontainer_name",
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+
+	t.Run("network_mode container fail", func(t *testing.T) {
+		project := &types.Project{
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:  "myservice1",
+					Image: "scratch",
+				},
+				{
+					Name:        "myservice2",
+					Image:       "scratch",
+					NetworkMode: "container:nonexistentcontainer",
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.Error(t, err, `service with container_name "nonexistentcontainer" not found for network_mode 'container:nonexistentcontainer'`)
+	})
+}
diff --git a/types/project_test.go b/types/project_test.go
index 47ee04d..b103731 100644
--- a/types/project_test.go
+++ b/types/project_test.go
@@ -45,7 +45,7 @@ func Test_NoProfiles(t *testing.T) {
 
 func Test_ServiceProfiles(t *testing.T) {
 	p := makeProject()
-	services, err := p.GetServices([]string{"service_1", "service_2"})
+	services, err := p.GetServices("service_1", "service_2")
 	assert.NilError(t, err)
 
 	profiles := services.GetProfiles()