Repository: compose-spec/compose-go
Pull request #126 (2021-04-20 14:59:42)

resolve build args and context path during `normalize` phase
Get compose model loading to resolve build context and Dockerfile paths into absolute paths, and resolve build args from configured environment variables (depends on configured sources: os.Env, .env, --env-file)
this will make Compose implementation code simpler as they don't have to handle this

see https://github.com/docker/compose-cli/issues/1540


Patch:
diff --git a/cli/options.go b/cli/options.go
index 4eff1e1..beb385f 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -121,14 +121,9 @@ func WithDotEnv(o *ProjectOptions) error {
 		}
 		dotEnvFile = filepath.Join(dir, dotEnvFile)
 	}
-	s, err := os.Stat(dotEnvFile)
-	if os.IsNotExist(err) {
+	if _, err := os.Stat(dotEnvFile); os.IsNotExist(err) {
 		return nil
 	}
-	if s.IsDir() {
-		return nil
-	}
-
 	file, err := os.Open(dotEnvFile)
 	if err != nil {
 		return err
diff --git a/loader/loader.go b/loader/loader.go
index cf82aa9..232cec0 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -191,14 +191,15 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 	}
 
 	project := &types.Project{
-		Name:       opts.Name,
-		WorkingDir: configDetails.WorkingDir,
-		Services:   model.Services,
-		Networks:   model.Networks,
-		Volumes:    model.Volumes,
-		Secrets:    model.Secrets,
-		Configs:    model.Configs,
-		Extensions: model.Extensions,
+		Name:        opts.Name,
+		WorkingDir:  configDetails.WorkingDir,
+		Services:    model.Services,
+		Networks:    model.Networks,
+		Volumes:     model.Volumes,
+		Secrets:     model.Secrets,
+		Configs:     model.Configs,
+		Environment: configDetails.Environment,
+		Extensions:  model.Extensions,
 	}
 
 	if !opts.SkipNormalization {
diff --git a/loader/normalize.go b/loader/normalize.go
index 871ebfa..9a1542b 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -18,6 +18,8 @@ package loader
 
 import (
 	"fmt"
+	"net/url"
+	"path/filepath"
 
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
@@ -47,6 +49,24 @@ func normalize(project *types.Project) error {
 			s.PullPolicy = types.PullPolicyMissing
 		}
 
+		fn := func(s string) (string, bool) {
+			v, ok := project.Environment[s]
+			return v, ok
+		}
+
+		if s.Build != nil {
+			if s.Build.Dockerfile == "" {
+				s.Build.Dockerfile = "Dockerfile"
+			}
+			_, err := url.ParseRequestURI(s.Build.Context)
+			if err != nil {
+				s.Build.Context = filepath.Join(project.WorkingDir, s.Build.Context)
+				s.Build.Dockerfile = filepath.Join(project.WorkingDir, s.Build.Context, s.Build.Dockerfile)
+			}
+			s.Build.Args = s.Build.Args.Resolve(fn)
+		}
+		s.Environment = s.Environment.Resolve(fn)
+
 		err := relocateLogDriver(s)
 		if err != nil {
 			return err
diff --git a/types/project.go b/types/project.go
index fd8495e..43c9400 100644
--- a/types/project.go
+++ b/types/project.go
@@ -29,15 +29,16 @@ import (
 
 // Project is the result of loading a set of compose files
 type Project struct {
-	Name         string     `yaml:"-" json:"-"`
-	WorkingDir   string     `yaml:"-" json:"-"`
-	Services     Services   `json:"services"`
-	Networks     Networks   `yaml:",omitempty" json:"networks,omitempty"`
-	Volumes      Volumes    `yaml:",omitempty" json:"volumes,omitempty"`
-	Secrets      Secrets    `yaml:",omitempty" json:"secrets,omitempty"`
-	Configs      Configs    `yaml:",omitempty" json:"configs,omitempty"`
-	Extensions   Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
-	ComposeFiles []string   `yaml:"-" json:"-"`
+	Name         string            `yaml:"-" json:"-"`
+	WorkingDir   string            `yaml:"-" json:"-"`
+	Services     Services          `json:"services"`
+	Networks     Networks          `yaml:",omitempty" json:"networks,omitempty"`
+	Volumes      Volumes           `yaml:",omitempty" json:"volumes,omitempty"`
+	Secrets      Secrets           `yaml:",omitempty" json:"secrets,omitempty"`
+	Configs      Configs           `yaml:",omitempty" json:"configs,omitempty"`
+	Extensions   Extensions        `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
+	ComposeFiles []string          `yaml:"-" json:"-"`
+	Environment  map[string]string `yaml:"-" json:"-"`
 
 	// DisabledServices track services which have been disable as profile is not active
 	DisabledServices Services `yaml:"-" json:"-"`
diff --git a/types/types.go b/types/types.go
index 5440cc4..3d445c2 100644
--- a/types/types.go
+++ b/types/types.go
@@ -764,12 +764,13 @@ type FileObjectConfig struct {
 }
 
 const (
-	// TypeServiceConditionHealthy is the type for waiting until a service is
-	// healthy.
+	// ServiceConditionCompletedSuccessfully is the type for waiting until a service has completed successfully (exit code 0).
+	ServiceConditionCompletedSuccessfully = "service_completed_successfully"
+
+	// ServiceConditionHealthy is the type for waiting until a service is healthy.
 	ServiceConditionHealthy = "service_healthy"
 
-	// TypeServiceConditionHealthy is the type for waiting until a service has
-	// started.
+	// ServiceConditionStarted is the type for waiting until a service has started (default).
 	ServiceConditionStarted = "service_started"
 )
 


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 5448520..7917586 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -597,8 +597,9 @@ networks:
 	workingDir, err := os.Getwd()
 	assert.NilError(t, err)
 	expected := &types.Project{
-		Name:       "",
-		WorkingDir: workingDir,
+		Name:        "",
+		Environment: map[string]string{"thebool": "true", "thefloat": "3.14", "theint": "555"},
+		WorkingDir:  workingDir,
 		Services: []types.ServiceConfig{
 			{
 				Name: "web",
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index a448ab6..ab70ada 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -17,15 +17,22 @@
 package loader
 
 import (
+	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/compose-spec/compose-go/types"
+	"gopkg.in/yaml.v2"
 	"gotest.tools/v3/assert"
 )
 
 func TestNormalizeNetworkNames(t *testing.T) {
 	project := types.Project{
-		Name: "myProject",
+		Name:       "myProject",
+		WorkingDir: "/some/path",
+		Environment: map[string]string{
+			"FOO": "BAR",
+		},
 		Networks: types.Networks{
 			"myExternalnet": {
 				Name:     "myExternalnet", // this is automaticaly setup by loader for externa networks before reaching normalization
@@ -36,25 +43,46 @@ func TestNormalizeNetworkNames(t *testing.T) {
 				Name: "CustomName",
 			},
 		},
-	}
-
-	expected := types.Project{
-		Name: "myProject",
-		Networks: types.Networks{
-			"default": {Name: "myProject_default"},
-			"myExternalnet": {
-				Name:     "myExternalnet",
-				External: types.External{External: true},
-			},
-			"mynet": {Name: "myProject_mynet"},
-			"myNamedNet": {
-				Name: "CustomName",
+		Services: []types.ServiceConfig{
+			{
+				Name: "foo",
+				Build: &types.BuildConfig{
+					Context: "foo",
+					Args: map[string]*string{
+						"FOO": nil,
+						"ZOT": nil,
+					},
+				},
 			},
 		},
 	}
+
+	expected := strings.ReplaceAll(`services:
+  foo:
+    build:
+      context: /some/path/foo
+      dockerfile: /some/path/some/path/foo/Dockerfile
+      args:
+        FOO: BAR
+        ZOT: null
+    networks:
+      default: null
+networks:
+  default:
+    name: myProject_default
+  myExternalnet:
+    name: myExternalnet
+    external: true
+  myNamedNet:
+    name: CustomName
+  mynet:
+    name: myProject_mynet
+`, "/", string(filepath.Separator))
 	err := normalize(&project)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, expected, project)
+	marshal, err := yaml.Marshal(project)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expected, string(marshal))
 }
 
 func TestNormalizeVolumes(t *testing.T) {