Skip to content

Commit 2f8259f

Browse files
committed
Merge pull request #344 from glours/replace-compose-ref-by-compose
Signed-off-by: Nicolas De Loof <[email protected]>
2 parents 8a78100 + b222198 commit 2f8259f

File tree

7 files changed

+126
-37
lines changed

7 files changed

+126
-37
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Go reference library for parsing and loading Compose files as specified by the
77

88
## Used by
99

10-
* [compose-ref](https://github.com/compose-spec/compose-ref)
10+
* [compose](https://github.com/docker/compose)
1111
* [containerd/nerdctl](https://github.com/containerd/nerdctl)
1212
* [compose-cli](https://github.com/docker/compose-cli)
1313
* [tilt.dev](https://github.com/tilt-dev/tilt)

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

+20-14
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"
@@ -59,6 +56,8 @@ type Options struct {
5956
SkipConsistencyCheck bool
6057
// Skip extends
6158
SkipExtends bool
59+
// Skip loading of env_files
60+
SkipResolveEnvFiles bool
6261
// Interpolation options
6362
Interpolate *interp.Options
6463
// Discard 'env_file' entries after resolving to 'environment' section
@@ -67,6 +66,8 @@ type Options struct {
6766
projectName string
6867
// Indicates when the projectName was imperatively set or guessed from path
6968
projectNameImperativelySet bool
69+
// Profiles set profiles to enable
70+
Profiles []string
7071
}
7172

7273
func (o *Options) SetProjectName(name string, imperativelySet bool) {
@@ -125,6 +126,13 @@ func WithSkipValidation(opts *Options) {
125126
opts.SkipValidation = true
126127
}
127128

129+
// WithProfiles sets profiles to be activated
130+
func WithProfiles(profiles []string) func(*Options) {
131+
return func(opts *Options) {
132+
opts.Profiles = profiles
133+
}
134+
}
135+
128136
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
129137
// structure, and returns it.
130138
func ParseYAML(source []byte) (map[string]interface{}, error) {
@@ -195,12 +203,6 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
195203
if err != nil {
196204
return nil, err
197205
}
198-
if opts.discardEnvFiles {
199-
for i := range cfg.Services {
200-
cfg.Services[i].EnvFile = nil
201-
}
202-
}
203-
204206
configs = append(configs, cfg)
205207
}
206208

@@ -243,7 +245,15 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
243245
}
244246
}
245247

246-
return project, nil
248+
if len(opts.Profiles) > 0 {
249+
project.ApplyProfiles(opts.Profiles)
250+
}
251+
252+
if !opts.SkipResolveEnvFiles {
253+
err = project.ResolveServicesEnvironment(opts.discardEnvFiles)
254+
}
255+
256+
return project, err
247257
}
248258

249259
func projectName(details types.ConfigDetails, opts *Options) string {
@@ -587,10 +597,6 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
587597
}
588598
serviceConfig.Name = name
589599

590-
if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
591-
return nil, err
592-
}
593-
594600
for i, volume := range serviceConfig.Volumes {
595601
if volume.Type != types.VolumeTypeBind {
596602
continue

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

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

1919
import (
20+
"bytes"
2021
"fmt"
22+
"github.com/pkg/errors"
23+
"io"
2124
"os"
2225
"path/filepath"
2326
"sort"
2427

28+
"github.com/compose-spec/compose-go/dotenv"
2529
"github.com/distribution/distribution/v3/reference"
2630
godigest "github.com/opencontainers/go-digest"
2731
"golang.org/x/sync/errgroup"
2832
)
2933

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

4347
// DisabledServices track services which have been disable as profile is not active
4448
DisabledServices Services `yaml:"-" json:"-"`
@@ -353,3 +357,49 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D
353357
}
354358
return eg.Wait()
355359
}
360+
361+
// ResolveServicesEnvironment parse env_files set for services to resolve the actual environment map for services
362+
func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
363+
for i, service := range p.Services {
364+
service.Environment = service.Environment.Resolve(p.Environment.Resolve)
365+
366+
environment := MappingWithEquals{}
367+
// resolve variables based on other files we already parsed, + project's environment
368+
var resolve dotenv.LookupFn = func(s string) (string, bool) {
369+
v, ok := environment[s]
370+
if ok && v != nil {
371+
return *v, ok
372+
}
373+
return p.Environment.Resolve(s)
374+
}
375+
376+
for _, envFile := range service.EnvFile {
377+
file, err := os.Open(envFile)
378+
if err != nil {
379+
return errors.Wrapf(err, "Failed to read %s", file)
380+
}
381+
382+
b, err := io.ReadAll(file)
383+
if err != nil {
384+
return errors.Wrapf(err, "Failed to read %s", file)
385+
}
386+
387+
// Do not defer to avoid it inside a loop
388+
file.Close() //nolint:errcheck
389+
390+
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
391+
if err != nil {
392+
return errors.Wrapf(err, "Failed to parse %s", file)
393+
}
394+
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
395+
}
396+
397+
service.Environment = environment.OverrideBy(service.Environment)
398+
399+
if discardEnvFiles {
400+
service.EnvFile = nil
401+
}
402+
p.Services[i] = service
403+
}
404+
return nil
405+
}

types/types.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,12 @@ type ThrottleDevice struct {
357357
// ShellCommand is a string or list of string args.
358358
//
359359
// When marshaled to YAML, nil command fields will be omitted if `omitempty`
360-
// is specified as a struct tag. Explicitly empty commands (i.e. `[]` or `''`)
360+
// is specified as a struct tag. Explicitly empty commands (i.e. `[]` or ``)
361361
// will serialize to an empty array (`[]`).
362362
//
363363
// When marshaled to JSON, the `omitempty` struct must NOT be specified.
364364
// If the command field is nil, it will be serialized as `null`.
365-
// Explicitly empty commands (i.e. `[]` or `''`) will serialize to an empty
365+
// Explicitly empty commands (i.e. `[]` or ``) will serialize to an empty
366366
// array (`[]`).
367367
//
368368
// The distinction between nil and explicitly empty is important to distinguish
@@ -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)