Repository: compose-spec/compose-go
Pull request #165 (2021-08-06 12:18:58)

Inherited dot env variables from a fork
This fork is necessary until the feature of  `godotenv.ParseWithLookup` is merged (with this [PR](https://github.com/joho/godotenv/pull/149) or any similar) so that empty variables can be Looked up from the environment


Patch:
diff --git a/cli/options.go b/cli/options.go
index d59780c..fe323cf 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -27,9 +27,9 @@ import (
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/loader"
 	"github.com/compose-spec/compose-go/types"
-	"github.com/joho/godotenv"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	"github.com/ulyssessouza/godotenv"
 )
 
 // ProjectOptions groups the command line options recommended for a Compose implementation
@@ -204,11 +204,22 @@ func WithDotEnv(o *ProjectOptions) error {
 	}
 	defer file.Close()
 
-	env, err := godotenv.Parse(file)
+	notInEnvSet := make(map[string]interface{})
+	env, err := godotenv.ParseWithLookup(file, func(k string) (string, bool) {
+		v, ok := os.LookupEnv(k)
+		if !ok {
+			notInEnvSet[k] = nil
+			return "", true
+		}
+		return v, true
+	})
 	if err != nil {
 		return err
 	}
 	for k, v := range env {
+		if _, ok := notInEnvSet[k]; ok {
+			continue
+		}
 		o.Environment[k] = v
 	}
 	return nil
diff --git a/go.mod b/go.mod
index 298251e..f151c6a 100644
--- a/go.mod
+++ b/go.mod
@@ -8,12 +8,12 @@ require (
 	github.com/docker/go-units v0.4.0
 	github.com/google/go-cmp v0.5.5
 	github.com/imdario/mergo v0.3.12
-	github.com/joho/godotenv v1.3.0
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/mitchellh/mapstructure v1.4.1
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.8.1
+	github.com/ulyssessouza/godotenv v1.3.1-0.20210806120901-e417b721114e
 	github.com/xeipuuv/gojsonschema v1.2.0
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
diff --git a/go.sum b/go.sum
index 59dbb95..40f4a28 100644
--- a/go.sum
+++ b/go.sum
@@ -51,8 +51,6 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
-github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
-github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -108,6 +106,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/ulyssessouza/godotenv v1.3.1-0.20210806120901-e417b721114e h1:byEYm3QADv5mDUesYKstWwRodf2RoxxC/YuGOxtdqJw=
+github.com/ulyssessouza/godotenv v1.3.1-0.20210806120901-e417b721114e/go.mod h1:9JN/BuU6Agy5aHyEoA5EIPkBsYbk0+2R42zJgYi/SlI=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
diff --git a/loader/loader.go b/loader/loader.go
index de145b5..a977257 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -33,11 +33,11 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	units "github.com/docker/go-units"
 	"github.com/imdario/mergo"
-	"github.com/joho/godotenv"
 	shellwords "github.com/mattn/go-shellwords"
 	"github.com/mitchellh/mapstructure"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	"github.com/ulyssessouza/godotenv"
 	yaml "gopkg.in/yaml.v2"
 )
 
diff --git a/loader/merge.go b/loader/merge.go
index b6bac39..3743841 100644
--- a/loader/merge.go
+++ b/loader/merge.go
@@ -33,7 +33,6 @@ var serviceSpecials = &specials{
 	m: map[reflect.Type]func(dst, src reflect.Value) error{
 		reflect.TypeOf(&types.LoggingConfig{}):           safelyMerge(mergeLoggingConfig),
 		reflect.TypeOf(&types.UlimitsConfig{}):           safelyMerge(mergeUlimitsConfig),
-		reflect.TypeOf([]types.ServiceVolumeConfig{}):    mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
 		reflect.TypeOf([]types.ServicePortConfig{}):      mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
 		reflect.TypeOf([]types.ServiceSecretConfig{}):    mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
 		reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
@@ -142,18 +141,6 @@ func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error)
 	return m, nil
 }
 
-func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
-	volumes, ok := s.([]types.ServiceVolumeConfig)
-	if !ok {
-		return nil, errors.Errorf("not a ServiceVolumeConfig slice: %v", s)
-	}
-	m := map[interface{}]interface{}{}
-	for _, v := range volumes {
-		m[v.Target] = v
-	}
-	return m, nil
-}
-
 func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
 	s := []types.ServiceSecretConfig{}
 	for _, v := range m {
@@ -184,16 +171,6 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{})
 	return nil
 }
 
-func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
-	s := []types.ServiceVolumeConfig{}
-	for _, v := range m {
-		s = append(s, v.(types.ServiceVolumeConfig))
-	}
-	sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target })
-	dst.Set(reflect.ValueOf(s))
-	return nil
-}
-
 type tomapFn func(s interface{}) (map[interface{}]interface{}, error)
 type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
 
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 6d9c7c2..cefe476 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -769,7 +769,7 @@
           "type": "object",
           "patternProperties": {
             ".+": {
-              "type": ["string", "number", "boolean", "null"]
+              "type": ["string", "number", "null"]
             }
           },
           "additionalProperties": false
diff --git a/template/template.go b/template/template.go
index d5ad0ae..2f91f00 100644
--- a/template/template.go
+++ b/template/template.go
@@ -20,8 +20,6 @@ import (
 	"fmt"
 	"regexp"
 	"strings"
-
-	"github.com/sirupsen/logrus"
 )
 
 var delimiter = "\\$"
@@ -99,10 +97,7 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
 			return value
 		}
 
-		value, ok := mapping(substitution)
-		if !ok {
-			logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
-		}
+		value, _ := mapping(substitution)
 		return value
 	})
 
diff --git a/types/config.go b/types/config.go
index 21127f4..1654412 100644
--- a/types/config.go
+++ b/types/config.go
@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 
 	"github.com/mitchellh/mapstructure"
+	"github.com/sirupsen/logrus"
 )
 
 // ConfigDetails are the details about a group of ConfigFiles
@@ -33,6 +34,9 @@ type ConfigDetails struct {
 // LookupEnv provides a lookup function for environment variables
 func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
 	v, ok := cd.Environment[key]
+	if !ok {
+		logrus.Warnf("The %s variable is not set. Defaulting to a blank string.", key)
+	}
 	return v, ok
 }
 
diff --git a/types/types.go b/types/types.go
index 37238dd..608b3a5 100644
--- a/types/types.go
+++ b/types/types.go
@@ -558,7 +558,7 @@ type ServiceNetworkConfig struct {
 // ServicePortConfig is the port configuration for a service
 type ServicePortConfig struct {
 	Mode      string `yaml:",omitempty" json:"mode,omitempty"`
-	HostIP    string `mapstructure:"host_ip" yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
+	HostIP    string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
 	Target    uint32 `yaml:",omitempty" json:"target,omitempty"`
 	Published uint32 `yaml:",omitempty" json:"published,omitempty"`
 	Protocol  string `yaml:",omitempty" json:"protocol,omitempty"`


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index d79f80f..44b63a5 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -456,29 +456,6 @@ services:
 	}
 }
 
-func TestLoadEnvironmentWithBoolean(t *testing.T) {
-	config, err := loadYAML(`
-services:
-  dict-env:
-    image: busybox
-    environment:
-      FOO: true
-      BAR: false
-`)
-	assert.NilError(t, err)
-
-	expected := types.MappingWithEquals{
-		"FOO": strPtr("true"),
-		"BAR": strPtr("false"),
-	}
-
-	assert.Check(t, is.Equal(1, len(config.Services)))
-
-	for _, service := range config.Services {
-		assert.Check(t, is.DeepEqual(expected, service.Environment))
-	}
-}
-
 func TestInvalidEnvironmentValue(t *testing.T) {
 	_, err := loadYAML(`
 services:
@@ -487,7 +464,7 @@ services:
     environment:
       FOO: ["1"]
 `)
-	assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number, boolean or null")
+	assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number or null")
 }
 
 func TestInvalidEnvironmentObject(t *testing.T) {