Skip to content

Commit ddc8c41

Browse files
authored
Merge pull request #338 from ndeloof/ResolveServicesEnvironment
closes docker/compose#8713
2 parents 559e4fd + a30b5f7 commit ddc8c41

File tree

6 files changed

+110
-80
lines changed

6 files changed

+110
-80
lines changed

cli/options.go

+8
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
167167
}
168168
}
169169

170+
// WithProfiles sets profiles to be activated
171+
func WithProfiles(profiles []string) ProjectOptionsFn {
172+
return func(o *ProjectOptions) error {
173+
o.loadOptions = append(o.loadOptions, loader.WithProfiles(profiles))
174+
return nil
175+
}
176+
}
177+
170178
// WithOsEnv imports environment variables from OS
171179
func WithOsEnv(o *ProjectOptions) error {
172180
for k, v := range utils.GetAsEqualsMap(os.Environ()) {

loader/loader.go

+16-60
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
package loader
1818

1919
import (
20-
"bytes"
2120
"fmt"
22-
"io"
2321
"os"
2422
paths "path"
2523
"path/filepath"
@@ -30,7 +28,6 @@ import (
3028
"time"
3129

3230
"github.com/compose-spec/compose-go/consts"
33-
"github.com/compose-spec/compose-go/dotenv"
3431
interp "github.com/compose-spec/compose-go/interpolation"
3532
"github.com/compose-spec/compose-go/schema"
3633
"github.com/compose-spec/compose-go/template"
@@ -67,6 +64,8 @@ type Options struct {
6764
projectName string
6865
// Indicates when the projectName was imperatively set or guessed from path
6966
projectNameImperativelySet bool
67+
// Profiles set profiles to enable
68+
Profiles []string
7069
}
7170

7271
func (o *Options) SetProjectName(name string, imperativelySet bool) {
@@ -125,6 +124,13 @@ func WithSkipValidation(opts *Options) {
125124
opts.SkipValidation = true
126125
}
127126

127+
// WithProfiles sets profiles to be activated
128+
func WithProfiles(profiles []string) func(*Options) {
129+
return func(opts *Options) {
130+
opts.Profiles = profiles
131+
}
132+
}
133+
128134
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
129135
// structure, and returns it.
130136
func ParseYAML(source []byte) (map[string]interface{}, error) {
@@ -198,12 +204,6 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
198204
if err != nil {
199205
return nil, err
200206
}
201-
if opts.discardEnvFiles {
202-
for i := range cfg.Services {
203-
cfg.Services[i].EnvFile = nil
204-
}
205-
}
206-
207207
configs = append(configs, cfg)
208208
}
209209

@@ -246,7 +246,13 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
246246
}
247247
}
248248

249-
return project, nil
249+
if len(opts.Profiles) > 0 {
250+
project.ApplyProfiles(opts.Profiles)
251+
}
252+
253+
err = project.ResolveServicesEnvironment(opts.discardEnvFiles)
254+
255+
return project, err
250256
}
251257

252258
func projectName(details types.ConfigDetails, opts *Options) (string, error) {
@@ -601,10 +607,6 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
601607
}
602608
serviceConfig.Name = name
603609

604-
if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
605-
return nil, err
606-
}
607-
608610
for i, volume := range serviceConfig.Volumes {
609611
if volume.Type != types.VolumeTypeBind {
610612
continue
@@ -641,52 +643,6 @@ func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConf
641643
return volume
642644
}
643645

644-
func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
645-
environment := types.MappingWithEquals{}
646-
var resolve dotenv.LookupFn = func(s string) (string, bool) {
647-
if v, ok := environment[s]; ok && v != nil {
648-
return *v, true
649-
}
650-
return lookupEnv(s)
651-
}
652-
653-
if len(serviceConfig.EnvFile) > 0 {
654-
if serviceConfig.Environment == nil {
655-
serviceConfig.Environment = types.MappingWithEquals{}
656-
}
657-
for _, envFile := range serviceConfig.EnvFile {
658-
filePath := absPath(workingDir, envFile)
659-
file, err := os.Open(filePath)
660-
if err != nil {
661-
return err
662-
}
663-
664-
b, err := io.ReadAll(file)
665-
if err != nil {
666-
return err
667-
}
668-
669-
// Do not defer to avoid it inside a loop
670-
file.Close() //nolint:errcheck
671-
672-
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
673-
if err != nil {
674-
return errors.Wrapf(err, "Failed to load %s", filePath)
675-
}
676-
env := types.MappingWithEquals{}
677-
for k, v := range fileVars {
678-
v := v
679-
env[k] = &v
680-
}
681-
environment.OverrideBy(env.Resolve(lookupEnv).RemoveEmpty())
682-
}
683-
}
684-
685-
environment.OverrideBy(serviceConfig.Environment.Resolve(lookupEnv))
686-
serviceConfig.Environment = environment
687-
return nil
688-
}
689-
690646
func resolveMaybeUnixPath(path string, workingDir string, lookupEnv template.Mapping) string {
691647
filePath := expandUser(path, lookupEnv)
692648
// Check if source is an absolute path (either Unix or Windows), to

loader/loader_test.go

+17-10
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,9 @@ func TestDiscardEnvFileOption(t *testing.T) {
814814
configDetails := buildConfigDetails(dict, nil)
815815

816816
// Default behavior keeps the `env_file` entries
817-
configWithEnvFiles, err := Load(configDetails)
817+
configWithEnvFiles, err := Load(configDetails, func(options *Options) {
818+
options.SkipNormalization = true
819+
})
818820
assert.NilError(t, err)
819821
assert.DeepEqual(t, configWithEnvFiles.Services[0].EnvFile, types.StringList{"example1.env",
820822
"example2.env"})
@@ -1936,17 +1938,22 @@ func TestLoadServiceWithEnvFile(t *testing.T) {
19361938
_, err = file.Write([]byte("HALLO=$TEST"))
19371939
assert.NilError(t, err)
19381940

1939-
m := map[string]interface{}{
1940-
"env_file": file.Name(),
1941+
p := &types.Project{
1942+
Environment: map[string]string{
1943+
"TEST": "YES",
1944+
},
1945+
Services: []types.ServiceConfig{
1946+
{
1947+
Name: "Test",
1948+
EnvFile: []string{file.Name()},
1949+
},
1950+
},
19411951
}
1942-
s, err := LoadService("Test Name", m, ".", func(s string) (string, bool) {
1943-
if s == "TEST" {
1944-
return "YES", true
1945-
}
1946-
return "", false
1947-
}, true, false)
1952+
err = p.ResolveServicesEnvironment(false)
1953+
assert.NilError(t, err)
1954+
service, err := p.GetService("Test")
19481955
assert.NilError(t, err)
1949-
assert.Equal(t, "YES", *s.Environment["HALLO"])
1956+
assert.Equal(t, "YES", *service.Environment["HALLO"])
19501957
}
19511958

19521959
func TestLoadServiceWithVolumes(t *testing.T) {

loader/normalize.go

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ func normalize(project *types.Project, resolvePaths bool) error {
8585
}
8686
s.Build.Args = s.Build.Args.Resolve(fn)
8787
}
88+
for j, f := range s.EnvFile {
89+
s.EnvFile[j] = absPath(project.WorkingDir, f)
90+
}
8891
s.Environment = s.Environment.Resolve(fn)
8992

9093
err := relocateLogDriver(&s)

types/project.go

+51-10
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,31 @@
1717
package types
1818

1919
import (
20+
"bytes"
2021
"fmt"
2122
"os"
2223
"path/filepath"
2324
"sort"
2425

26+
"github.com/compose-spec/compose-go/dotenv"
2527
"github.com/distribution/distribution/v3/reference"
2628
godigest "github.com/opencontainers/go-digest"
29+
"github.com/pkg/errors"
2730
"golang.org/x/sync/errgroup"
2831
)
2932

3033
// Project is the result of loading a set of compose files
3134
type Project struct {
32-
Name string `yaml:"name,omitempty" json:"name,omitempty"`
33-
WorkingDir string `yaml:"-" json:"-"`
34-
Services Services `json:"services"`
35-
Networks Networks `yaml:",omitempty" json:"networks,omitempty"`
36-
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`
37-
Secrets Secrets `yaml:",omitempty" json:"secrets,omitempty"`
38-
Configs Configs `yaml:",omitempty" json:"configs,omitempty"`
39-
Extensions Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
40-
ComposeFiles []string `yaml:"-" json:"-"`
41-
Environment map[string]string `yaml:"-" json:"-"`
35+
Name string `yaml:"name,omitempty" json:"name,omitempty"`
36+
WorkingDir string `yaml:"-" json:"-"`
37+
Services Services `json:"services"`
38+
Networks Networks `yaml:",omitempty" json:"networks,omitempty"`
39+
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`
40+
Secrets Secrets `yaml:",omitempty" json:"secrets,omitempty"`
41+
Configs Configs `yaml:",omitempty" json:"configs,omitempty"`
42+
Extensions Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
43+
ComposeFiles []string `yaml:"-" json:"-"`
44+
Environment Mapping `yaml:"-" json:"-"`
4245

4346
// DisabledServices track services which have been disable as profile is not active
4447
DisabledServices Services `yaml:"-" json:"-"`
@@ -353,3 +356,41 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D
353356
}
354357
return eg.Wait()
355358
}
359+
360+
// ResolveServicesEnvironment parse env_files set for services to resolve the actual environment map for services
361+
func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
362+
for i, service := range p.Services {
363+
service.Environment = service.Environment.Resolve(p.Environment.Resolve)
364+
365+
environment := MappingWithEquals{}
366+
// resolve variables based on other files we already parsed, + project's environment
367+
var resolve dotenv.LookupFn = func(s string) (string, bool) {
368+
v, ok := environment[s]
369+
if ok && v != nil {
370+
return *v, ok
371+
}
372+
return p.Environment.Resolve(s)
373+
}
374+
375+
for _, envFile := range service.EnvFile {
376+
b, err := os.ReadFile(envFile)
377+
if err != nil {
378+
return errors.Wrapf(err, "Failed to load %s", envFile)
379+
}
380+
381+
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
382+
if err != nil {
383+
return err
384+
}
385+
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
386+
}
387+
388+
service.Environment = environment.OverrideBy(service.Environment)
389+
390+
if discardEnvFiles {
391+
service.EnvFile = nil
392+
}
393+
p.Services[i] = service
394+
}
395+
return nil
396+
}

types/types.go

+15
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,21 @@ func NewMapping(values []string) Mapping {
482482
return mapping
483483
}
484484

485+
// ToMappingWithEquals converts Mapping into a MappingWithEquals with pointer references
486+
func (m Mapping) ToMappingWithEquals() MappingWithEquals {
487+
mapping := MappingWithEquals{}
488+
for k, v := range m {
489+
v := v
490+
mapping[k] = &v
491+
}
492+
return mapping
493+
}
494+
495+
func (m Mapping) Resolve(s string) (string, bool) {
496+
v, ok := m[s]
497+
return v, ok
498+
}
499+
485500
// Labels is a mapping type for labels
486501
type Labels map[string]string
487502

0 commit comments

Comments
 (0)