Skip to content

Commit 8f1be26

Browse files
ndeloofglours
authored andcommitted
restore support for !reset on extends
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 3f05c82 commit 8f1be26

File tree

3 files changed

+142
-121
lines changed

3 files changed

+142
-121
lines changed

loader/extends.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/compose-spec/compose-go/v2/consts"
2525
"github.com/compose-spec/compose-go/v2/override"
26+
"github.com/compose-spec/compose-go/v2/paths"
2627
"github.com/compose-spec/compose-go/v2/types"
2728
)
2829

@@ -75,10 +76,15 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
7576
opts.ProcessEvent("extends", map[string]any{"service": ref})
7677
}
7778

78-
var base any
79+
var (
80+
base any
81+
processor PostProcessor
82+
)
83+
7984
if file != nil {
8085
filename = file.(string)
81-
services, err = getExtendsBaseFromFile(ctx, ref, filename, opts, tracker)
86+
services, processor, err = getExtendsBaseFromFile(ctx, ref, filename, opts, tracker)
87+
post = append(post, processor)
8288
if err != nil {
8389
return nil, err
8490
}
@@ -121,14 +127,14 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
121127
return merged, nil
122128
}
123129

124-
func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) {
130+
func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, PostProcessor, error) {
125131
for _, loader := range opts.ResourceLoaders {
126132
if !loader.Accept(path) {
127133
continue
128134
}
129135
local, err := loader.Load(ctx, path)
130136
if err != nil {
131-
return nil, err
137+
return nil, nil, err
132138
}
133139
localdir := filepath.Dir(local)
134140
relworkingdir := loader.Dir(path)
@@ -138,30 +144,36 @@ func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts
138144
extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{
139145
WorkingDir: localdir,
140146
})
141-
extendsOpts.ResolvePaths = true
147+
extendsOpts.ResolvePaths = false // we do relative path resolution after file has been loaded
142148
extendsOpts.SkipNormalization = true
143149
extendsOpts.SkipConsistencyCheck = true
144150
extendsOpts.SkipInclude = true
145151
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
146152
extendsOpts.SkipValidation = true // we validate the merge result
147153
extendsOpts.SkipDefaultValues = true
148-
source, err := loadYamlModel(ctx, types.ConfigDetails{
149-
WorkingDir: relworkingdir,
150-
ConfigFiles: []types.ConfigFile{
151-
{Filename: local},
152-
},
153-
}, extendsOpts, ct, nil)
154+
source, processor, err := loadYamlFile(ctx, types.ConfigFile{Filename: local},
155+
extendsOpts, relworkingdir, nil, ct, map[string]any{}, nil)
154156
if err != nil {
155-
return nil, err
157+
return nil, nil, err
156158
}
157159
services := source["services"].(map[string]any)
158160
_, ok := services[name]
159161
if !ok {
160-
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, path)
162+
return nil, nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, path)
161163
}
162-
return services, nil
164+
165+
var remotes []paths.RemoteResource
166+
for _, loader := range opts.RemoteResourceLoaders() {
167+
remotes = append(remotes, loader.Accept)
168+
}
169+
err = paths.ResolveRelativePaths(source, relworkingdir, remotes)
170+
if err != nil {
171+
return nil, nil, err
172+
}
173+
174+
return services, processor, nil
163175
}
164-
return nil, fmt.Errorf("cannot read %s", path)
176+
return nil, nil, fmt.Errorf("cannot read %s", path)
165177
}
166178

167179
func deepClone(value any) any {

loader/include.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,17 @@ func loadIncludeConfig(source any) ([]types.IncludeConfig, error) {
5050
return requires, err
5151
}
5252

53-
func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model map[string]any, options *Options, included []string) error {
53+
func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string) error {
5454
includeConfig, err := loadIncludeConfig(model["include"])
5555
if err != nil {
5656
return err
5757
}
58+
5859
for _, r := range includeConfig {
5960
for _, listener := range options.Listeners {
6061
listener("include", map[string]any{
6162
"path": r.Path,
62-
"workingdir": configDetails.WorkingDir,
63+
"workingdir": workingDir,
6364
})
6465
}
6566

@@ -83,7 +84,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
8384
r.ProjectDirectory = filepath.Dir(path)
8485
case !filepath.IsAbs(r.ProjectDirectory):
8586
relworkingdir = loader.Dir(r.ProjectDirectory)
86-
r.ProjectDirectory = filepath.Join(configDetails.WorkingDir, r.ProjectDirectory)
87+
r.ProjectDirectory = filepath.Join(workingDir, r.ProjectDirectory)
8788

8889
default:
8990
relworkingdir = r.ProjectDirectory
@@ -117,7 +118,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
117118
envFile := []string{}
118119
for _, f := range r.EnvFile {
119120
if !filepath.IsAbs(f) {
120-
f = filepath.Join(configDetails.WorkingDir, f)
121+
f = filepath.Join(workingDir, f)
121122
s, err := os.Stat(f)
122123
if err != nil {
123124
return err
@@ -131,15 +132,15 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
131132
r.EnvFile = envFile
132133
}
133134

134-
envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.EnvFile)
135+
envFromFile, err := dotenv.GetEnvFromFile(environment, r.EnvFile)
135136
if err != nil {
136137
return err
137138
}
138139

139140
config := types.ConfigDetails{
140141
WorkingDir: relworkingdir,
141142
ConfigFiles: types.ToConfigFiles(r.Path),
142-
Environment: configDetails.Environment.Clone().Merge(envFromFile),
143+
Environment: environment.Clone().Merge(envFromFile),
143144
}
144145
loadOptions.Interpolate = &interp.Options{
145146
Substitute: options.Interpolate.Substitute,

loader/loader.go

Lines changed: 108 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -358,140 +358,148 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
358358
dict = map[string]interface{}{}
359359
err error
360360
)
361+
workingDir, environment := config.WorkingDir, config.Environment
362+
361363
for _, file := range config.ConfigFiles {
362-
fctx := context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
363-
if file.Content == nil && file.Config == nil {
364-
content, err := os.ReadFile(file.Filename)
365-
if err != nil {
366-
return nil, err
367-
}
368-
file.Content = content
364+
dict, _, err = loadYamlFile(ctx, file, opts, workingDir, environment, ct, dict, included)
365+
if err != nil {
366+
return nil, err
369367
}
368+
}
370369

371-
processRawYaml := func(raw interface{}, processors ...PostProcessor) error {
372-
converted, err := convertToStringKeysRecursive(raw, "")
373-
if err != nil {
374-
return err
375-
}
376-
cfg, ok := converted.(map[string]interface{})
377-
if !ok {
378-
return errors.New("Top-level object must be a mapping")
379-
}
370+
if !opts.SkipDefaultValues {
371+
dict, err = transform.SetDefaultValues(dict)
372+
if err != nil {
373+
return nil, err
374+
}
375+
}
380376

381-
if opts.Interpolate != nil && !opts.SkipInterpolation {
382-
cfg, err = interp.Interpolate(cfg, *opts.Interpolate)
383-
if err != nil {
384-
return err
385-
}
386-
}
377+
if !opts.SkipValidation {
378+
if err := validation.Validate(dict); err != nil {
379+
return nil, err
380+
}
381+
}
387382

388-
fixEmptyNotNull(cfg)
383+
if opts.ResolvePaths {
384+
var remotes []paths.RemoteResource
385+
for _, loader := range opts.RemoteResourceLoaders() {
386+
remotes = append(remotes, loader.Accept)
387+
}
388+
err = paths.ResolveRelativePaths(dict, config.WorkingDir, remotes)
389+
if err != nil {
390+
return nil, err
391+
}
392+
}
393+
resolveServicesEnvironment(dict, config)
389394

390-
if !opts.SkipExtends {
391-
err = ApplyExtends(fctx, cfg, opts, ct, processors...)
392-
if err != nil {
393-
return err
394-
}
395-
}
395+
return dict, nil
396+
}
396397

397-
for _, processor := range processors {
398-
if err := processor.Apply(dict); err != nil {
399-
return err
400-
}
401-
}
398+
func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, workingDir string, environment types.Mapping, ct *cycleTracker, dict map[string]interface{}, included []string) (map[string]interface{}, PostProcessor, error) {
399+
ctx = context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
400+
if file.Content == nil && file.Config == nil {
401+
content, err := os.ReadFile(file.Filename)
402+
if err != nil {
403+
return nil, nil, err
404+
}
405+
file.Content = content
406+
}
402407

403-
if !opts.SkipInclude {
404-
included = append(included, config.ConfigFiles[0].Filename)
405-
err = ApplyInclude(ctx, config, cfg, opts, included)
406-
if err != nil {
407-
return err
408-
}
409-
}
408+
processRawYaml := func(raw interface{}, processors ...PostProcessor) error {
409+
converted, err := convertToStringKeysRecursive(raw, "")
410+
if err != nil {
411+
return err
412+
}
413+
cfg, ok := converted.(map[string]interface{})
414+
if !ok {
415+
return errors.New("Top-level object must be a mapping")
416+
}
410417

411-
dict, err = override.Merge(dict, cfg)
418+
if opts.Interpolate != nil && !opts.SkipInterpolation {
419+
cfg, err = interp.Interpolate(cfg, *opts.Interpolate)
412420
if err != nil {
413421
return err
414422
}
423+
}
415424

416-
dict, err = override.EnforceUnicity(dict)
425+
fixEmptyNotNull(cfg)
426+
427+
if !opts.SkipExtends {
428+
err = ApplyExtends(ctx, cfg, opts, ct, processors...)
417429
if err != nil {
418430
return err
419431
}
432+
}
420433

421-
if !opts.SkipValidation {
422-
if err := schema.Validate(dict); err != nil {
423-
return fmt.Errorf("validating %s: %w", file.Filename, err)
424-
}
425-
if _, ok := dict["version"]; ok {
426-
opts.warnObsoleteVersion(file.Filename)
427-
delete(dict, "version")
428-
}
434+
for _, processor := range processors {
435+
if err := processor.Apply(dict); err != nil {
436+
return err
437+
}
438+
}
439+
440+
if !opts.SkipInclude {
441+
included = append(included, file.Filename)
442+
err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included)
443+
if err != nil {
444+
return err
429445
}
446+
}
430447

448+
dict, err = override.Merge(dict, cfg)
449+
if err != nil {
431450
return err
432451
}
433452

434-
if file.Config == nil {
435-
r := bytes.NewReader(file.Content)
436-
decoder := yaml.NewDecoder(r)
437-
for {
438-
var raw interface{}
439-
processor := &ResetProcessor{target: &raw}
440-
err := decoder.Decode(processor)
441-
if err != nil && errors.Is(err, io.EOF) {
442-
break
443-
}
444-
if err != nil {
445-
return nil, err
446-
}
447-
if err := processRawYaml(raw, processor); err != nil {
448-
return nil, err
449-
}
453+
dict, err = override.EnforceUnicity(dict)
454+
if err != nil {
455+
return err
456+
}
457+
458+
if !opts.SkipValidation {
459+
if err := schema.Validate(dict); err != nil {
460+
return fmt.Errorf("validating %s: %w", file.Filename, err)
450461
}
451-
} else {
452-
if err := processRawYaml(file.Config); err != nil {
453-
return nil, err
462+
if _, ok := dict["version"]; ok {
463+
opts.warnObsoleteVersion(file.Filename)
464+
delete(dict, "version")
454465
}
455466
}
456-
}
457-
458-
dict, err = transform.Canonical(dict, opts.SkipInterpolation)
459-
if err != nil {
460-
return nil, err
461-
}
462467

463-
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
464-
dict, err = override.EnforceUnicity(dict)
465-
if err != nil {
466-
return nil, err
467-
}
468-
469-
if !opts.SkipDefaultValues {
470-
dict, err = transform.SetDefaultValues(dict)
468+
dict, err = transform.Canonical(dict, opts.SkipInterpolation)
471469
if err != nil {
472-
return nil, err
470+
return err
473471
}
474-
}
475472

476-
if !opts.SkipValidation {
477-
if err := validation.Validate(dict); err != nil {
478-
return nil, err
479-
}
473+
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
474+
dict, err = override.EnforceUnicity(dict)
475+
return err
480476
}
481477

482-
if opts.ResolvePaths {
483-
var remotes []paths.RemoteResource
484-
for _, loader := range opts.RemoteResourceLoaders() {
485-
remotes = append(remotes, loader.Accept)
478+
var processor PostProcessor
479+
if file.Config == nil {
480+
r := bytes.NewReader(file.Content)
481+
decoder := yaml.NewDecoder(r)
482+
for {
483+
var raw interface{}
484+
reset := &ResetProcessor{target: &raw}
485+
err := decoder.Decode(reset)
486+
if err != nil && errors.Is(err, io.EOF) {
487+
break
488+
}
489+
if err != nil {
490+
return nil, nil, err
491+
}
492+
processor = reset
493+
if err := processRawYaml(raw, processor); err != nil {
494+
return nil, nil, err
495+
}
486496
}
487-
err = paths.ResolveRelativePaths(dict, config.WorkingDir, remotes)
488-
if err != nil {
489-
return nil, err
497+
} else {
498+
if err := processRawYaml(file.Config); err != nil {
499+
return nil, nil, err
490500
}
491501
}
492-
resolveServicesEnvironment(dict, config)
493-
494-
return dict, nil
502+
return dict, processor, nil
495503
}
496504

497505
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (map[string]interface{}, error) {

0 commit comments

Comments
 (0)