Skip to content

Commit b559569

Browse files
committed
introduce ResolveServicesEnvironment and resolve service environment AFTER profiles have been applied
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 559e4fd commit b559569

File tree

6 files changed

+109
-77
lines changed

6 files changed

+109
-77
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-57
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type Options struct {
6767
projectName string
6868
// Indicates when the projectName was imperatively set or guessed from path
6969
projectNameImperativelySet bool
70+
// Profiles set profiles to enable
71+
Profiles []string
7072
}
7173

7274
func (o *Options) SetProjectName(name string, imperativelySet bool) {
@@ -125,6 +127,13 @@ func WithSkipValidation(opts *Options) {
125127
opts.SkipValidation = true
126128
}
127129

130+
// WithProfiles sets profiles to be activated
131+
func WithProfiles(profiles []string) func(*Options) {
132+
return func(opts *Options) {
133+
opts.Profiles = profiles
134+
}
135+
}
136+
128137
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
129138
// structure, and returns it.
130139
func ParseYAML(source []byte) (map[string]interface{}, error) {
@@ -198,12 +207,6 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
198207
if err != nil {
199208
return nil, err
200209
}
201-
if opts.discardEnvFiles {
202-
for i := range cfg.Services {
203-
cfg.Services[i].EnvFile = nil
204-
}
205-
}
206-
207210
configs = append(configs, cfg)
208211
}
209212

@@ -246,7 +249,13 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
246249
}
247250
}
248251

249-
return project, nil
252+
if len(opts.Profiles) > 0 {
253+
project.ApplyProfiles(opts.Profiles)
254+
}
255+
256+
err = project.ResolveServicesEnvironment(opts.discardEnvFiles)
257+
258+
return project, err
250259
}
251260

252261
func projectName(details types.ConfigDetails, opts *Options) (string, error) {
@@ -601,10 +610,6 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
601610
}
602611
serviceConfig.Name = name
603612

604-
if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
605-
return nil, err
606-
}
607-
608613
for i, volume := range serviceConfig.Volumes {
609614
if volume.Type != types.VolumeTypeBind {
610615
continue
@@ -641,52 +646,6 @@ func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConf
641646
return volume
642647
}
643648

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-
690649
func resolveMaybeUnixPath(path string, workingDir string, lookupEnv template.Mapping) string {
691650
filePath := expandUser(path, lookupEnv)
692651
// 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

+50-10
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,30 @@
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"
2729
"golang.org/x/sync/errgroup"
2830
)
2931

3032
// Project is the result of loading a set of compose files
3133
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:"-"`
34+
Name string `yaml:"name,omitempty" json:"name,omitempty"`
35+
WorkingDir string `yaml:"-" json:"-"`
36+
Services Services `json:"services"`
37+
Networks Networks `yaml:",omitempty" json:"networks,omitempty"`
38+
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`
39+
Secrets Secrets `yaml:",omitempty" json:"secrets,omitempty"`
40+
Configs Configs `yaml:",omitempty" json:"configs,omitempty"`
41+
Extensions Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
42+
ComposeFiles []string `yaml:"-" json:"-"`
43+
Environment Mapping `yaml:"-" json:"-"`
4244

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

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)