Skip to content

Commit ef965fe

Browse files
gloursndeloof
authored andcommitted
make public functions of Project type returning a new Project object
Signed-off-by: Guillaume Lours <[email protected]>
1 parent 8585af8 commit ef965fe

File tree

9 files changed

+136
-96
lines changed

9 files changed

+136
-96
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ require (
2222

2323
require (
2424
github.com/davecgh/go-spew v1.1.1 // indirect
25+
github.com/mitchellh/copystructure v1.2.0 // indirect
26+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
2527
github.com/pmezard/go-difflib v1.0.0 // indirect
2628
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
2729
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1212
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1313
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
1414
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
15+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
16+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
1517
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
1618
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
19+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
20+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
1721
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
1822
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
1923
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

loader/loader.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,12 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
451451
}
452452
}
453453

454-
project.ApplyProfiles(opts.Profiles)
454+
if project, err = project.ApplyProfiles(opts.Profiles); err != nil {
455+
return nil, err
456+
}
455457

456458
if !opts.SkipResolveEnvironment {
457-
err := project.ResolveServicesEnvironment(opts.discardEnvFiles)
459+
project, err = project.ResolveServicesEnvironment(opts.discardEnvFiles)
458460
if err != nil {
459461
return nil, err
460462
}

loader/loader_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2131,7 +2131,7 @@ func TestLoadServiceWithEnvFile(t *testing.T) {
21312131
},
21322132
},
21332133
}
2134-
err = p.ResolveServicesEnvironment(false)
2134+
p, err = p.ResolveServicesEnvironment(false)
21352135
assert.NilError(t, err)
21362136
service, err := p.GetService("test")
21372137
assert.NilError(t, err)

types/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
)
2424

2525
func Test_WithServices(t *testing.T) {
26-
p := Project{
26+
p := &Project{
2727
Services: Services{
2828
"service_1": ServiceConfig{
2929
Name: "service_1",
@@ -54,7 +54,7 @@ func Test_WithServices(t *testing.T) {
5454
return nil
5555
}
5656

57-
err := p.WithServices(nil, fn)
57+
p, err := p.WithServices(nil, fn)
5858
assert.NilError(t, err)
5959
assert.DeepEqual(t, order, []string{"service_2", "service_3", "service_1"})
6060
}

types/project.go

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ import (
2727
"github.com/compose-spec/compose-go/v2/dotenv"
2828
"github.com/compose-spec/compose-go/v2/utils"
2929
"github.com/distribution/reference"
30+
"github.com/mitchellh/copystructure"
3031
godigest "github.com/opencontainers/go-digest"
3132
"github.com/pkg/errors"
3233
"golang.org/x/sync/errgroup"
3334
"gopkg.in/yaml.v3"
3435
)
3536

3637
// Project is the result of loading a set of compose files
38+
// Since v2, Project are managed as immutable objects.
39+
// Each public functions which mutate Project state now return a copy of the original Project with the expected changes.
3740
type Project struct {
3841
Name string `yaml:"name,omitempty" json:"name,omitempty"`
3942
WorkingDir string `yaml:"-" json:"-"`
@@ -185,13 +188,19 @@ func (p *Project) AllServices() Services {
185188

186189
type ServiceFunc func(name string, service ServiceConfig) error
187190

188-
// WithServices run ServiceFunc on each service and dependencies according to DependencyPolicy
189-
func (p *Project) WithServices(names []string, fn ServiceFunc, options ...DependencyOption) error {
191+
// WithServices runs ServiceFunc on each service and dependencies according to DependencyPolicy
192+
// It returns a new Project instance with the changes and keep the original Project unchanged
193+
func (p *Project) WithServices(names []string, fn ServiceFunc, options ...DependencyOption) (*Project, error) {
194+
newProject, err := p.deepCopy()
195+
if err != nil {
196+
return nil, err
197+
}
190198
if len(options) == 0 {
191199
// backward compatibility
192200
options = []DependencyOption{IncludeDependencies}
193201
}
194-
return p.withServices(names, fn, map[string]bool{}, options, map[string]ServiceDependency{})
202+
err = newProject.withServices(names, fn, map[string]bool{}, options, map[string]ServiceDependency{})
203+
return newProject, err
195204
}
196205

197206
type withServicesOptions struct {
@@ -291,53 +300,69 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
291300
}
292301

293302
// ApplyProfiles disables service which don't match selected profiles
294-
func (p *Project) ApplyProfiles(profiles []string) {
303+
// It returns a new Project instance with the changes and keep the original Project unchanged
304+
func (p *Project) ApplyProfiles(profiles []string) (*Project, error) {
305+
newProject, err := p.deepCopy()
306+
if err != nil {
307+
return nil, err
308+
}
295309
for _, p := range profiles {
296310
if p == "*" {
297-
return
311+
return newProject, nil
298312
}
299313
}
300314
enabled := Services{}
301315
disabled := Services{}
302-
for name, service := range p.AllServices() {
316+
for name, service := range newProject.AllServices() {
303317
if service.HasProfile(profiles) {
304318
enabled[name] = service
305319
} else {
306320
disabled[name] = service
307321
}
308322
}
309-
p.Services = enabled
310-
p.DisabledServices = disabled
311-
p.Profiles = profiles
323+
newProject.Services = enabled
324+
newProject.DisabledServices = disabled
325+
newProject.Profiles = profiles
326+
return newProject, nil
312327
}
313328

314-
// EnableServices ensure services are enabled and activate profiles accordingly
315-
func (p *Project) EnableServices(names ...string) error {
329+
// EnableServices ensures services are enabled and activate profiles accordingly
330+
// It returns a new Project instance with the changes and keep the original Project unchanged
331+
func (p *Project) EnableServices(names ...string) (*Project, error) {
332+
newProject, err := p.deepCopy()
333+
if err != nil {
334+
return nil, err
335+
}
316336
if len(names) == 0 {
317-
return nil
337+
return newProject, nil
318338
}
319339

320340
profiles := append([]string{}, p.Profiles...)
321341
for _, name := range names {
322-
if _, ok := p.Services[name]; ok {
342+
if _, ok := newProject.Services[name]; ok {
323343
// already enabled
324344
continue
325345
}
326346
service := p.DisabledServices[name]
327347
profiles = append(profiles, service.Profiles...)
328348
}
329-
p.ApplyProfiles(profiles)
349+
newProject, err = newProject.ApplyProfiles(profiles)
350+
if err != nil {
351+
return newProject, err
352+
}
330353

331-
return p.ResolveServicesEnvironment(true)
354+
return newProject.ResolveServicesEnvironment(true)
332355
}
333356

334357
// WithoutUnnecessaryResources drops networks/volumes/secrets/configs that are not referenced by active services
335-
func (p *Project) WithoutUnnecessaryResources() {
358+
// It returns a new Project instance with the changes and keep the original Project unchanged
359+
func (p *Project) WithoutUnnecessaryResources() (*Project, error) {
360+
newProject, err := p.deepCopy()
336361
requiredNetworks := map[string]struct{}{}
337362
requiredVolumes := map[string]struct{}{}
338363
requiredSecrets := map[string]struct{}{}
339364
requiredConfigs := map[string]struct{}{}
340-
for _, s := range p.Services {
365+
for _, s := range newProject.Services {
341366
for k := range s.Networks {
342367
requiredNetworks[k] = struct{}{}
343368
}
@@ -366,31 +391,32 @@ func (p *Project) WithoutUnnecessaryResources() {
366391
networks[k] = value
367392
}
368393
}
369-
p.Networks = networks
394+
newProject.Networks = networks
370395

371396
volumes := Volumes{}
372397
for k := range requiredVolumes {
373398
if value, ok := p.Volumes[k]; ok {
374399
volumes[k] = value
375400
}
376401
}
377-
p.Volumes = volumes
402+
newProject.Volumes = volumes
378403

379404
secrets := Secrets{}
380405
for k := range requiredSecrets {
381406
if value, ok := p.Secrets[k]; ok {
382407
secrets[k] = value
383408
}
384409
}
385-
p.Secrets = secrets
410+
newProject.Secrets = secrets
386411

387412
configs := Configs{}
388413
for k := range requiredConfigs {
389414
if value, ok := p.Configs[k]; ok {
390415
configs[k] = value
391416
}
392417
}
393-
p.Configs = configs
418+
newProject.Configs = configs
419+
return newProject, err
394420
}
395421

396422
type DependencyOption func(options *withServicesOptions)
@@ -407,25 +433,26 @@ func IgnoreDependencies(options *withServicesOptions) {
407433
options.dependencyPolicy = ignoreDependencies
408434
}
409435

410-
// ForServices restrict the project model to selected services and dependencies
411-
func (p *Project) ForServices(names []string, options ...DependencyOption) error {
436+
// ForServices restricts the project model to selected services and dependencies
437+
// It returns a new Project instance with the changes and keep the original Project unchanged
438+
func (p *Project) ForServices(names []string, options ...DependencyOption) (*Project, error) {
412439
if len(names) == 0 {
413440
// All services
414-
return nil
441+
return p.deepCopy()
415442
}
416443

417444
set := utils.NewSet[string]()
418-
err := p.WithServices(names, func(name string, service ServiceConfig) error {
445+
newProject, err := p.WithServices(names, func(name string, service ServiceConfig) error {
419446
set.Add(name)
420447
return nil
421448
}, options...)
422449
if err != nil {
423-
return err
450+
return nil, err
424451
}
425452

426453
// Disable all services which are not explicit target or dependencies
427454
enabled := Services{}
428-
for name, s := range p.Services {
455+
for name, s := range newProject.Services {
429456
if _, ok := set[name]; ok {
430457
// remove all dependencies but those implied by explicitly selected services
431458
dependencies := s.DependsOn
@@ -437,32 +464,46 @@ func (p *Project) ForServices(names []string, options ...DependencyOption) error
437464
s.DependsOn = dependencies
438465
enabled[name] = s
439466
} else {
440-
p.DisableService(s)
467+
if newProject, err = newProject.DisableService(s); err != nil {
468+
return nil, err
469+
}
441470
}
442471
}
443-
p.Services = enabled
444-
return nil
472+
newProject.Services = enabled
473+
return newProject, nil
445474
}
446475

447-
func (p *Project) DisableService(service ServiceConfig) {
476+
// DisableService removes from the project model the given service and its references in all dependencies
477+
// It returns a new Project instance with the changes and keep the original Project unchanged
478+
func (p *Project) DisableService(service ServiceConfig) (*Project, error) {
479+
newProject, err := p.deepCopy()
480+
if err != nil {
481+
return nil, err
482+
}
448483
// We should remove all dependencies which reference the disabled service
449-
for i, s := range p.Services {
484+
for i, s := range newProject.Services {
450485
if _, ok := s.DependsOn[service.Name]; ok {
451486
delete(s.DependsOn, service.Name)
452-
p.Services[i] = s
487+
newProject.Services[i] = s
453488
}
454489
}
455490
delete(p.Services, service.Name)
456-
if p.DisabledServices == nil {
457-
p.DisabledServices = Services{}
491+
if newProject.DisabledServices == nil {
492+
newProject.DisabledServices = Services{}
458493
}
459-
p.DisabledServices[service.Name] = service
494+
newProject.DisabledServices[service.Name] = service
495+
return newProject, err
460496
}
461497

462498
// ResolveImages updates services images to include digest computed by a resolver function
463-
func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) error {
499+
// It returns a new Project instance with the changes and keep the original Project unchanged
500+
func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) (*Project, error) {
501+
newProject, err := p.deepCopy()
502+
if err != nil {
503+
return nil, err
504+
}
464505
eg := errgroup.Group{}
465-
for i, s := range p.Services {
506+
for i, s := range newProject.Services {
466507
idx := i
467508
service := s
468509

@@ -488,11 +529,11 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D
488529
}
489530

490531
service.Image = named.String()
491-
p.Services[idx] = service
532+
newProject.Services[idx] = service
492533
return nil
493534
})
494535
}
495-
return eg.Wait()
536+
return newProject, eg.Wait()
496537
}
497538

498539
// MarshalYAML marshal Project into a yaml tree
@@ -533,10 +574,15 @@ func (p *Project) MarshalJSON() ([]byte, error) {
533574
return json.Marshal(m)
534575
}
535576

536-
// ResolveServicesEnvironment parse env_files set for services to resolve the actual environment map for services
537-
func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
538-
for i, service := range p.Services {
539-
service.Environment = service.Environment.Resolve(p.Environment.Resolve)
577+
// ResolveServicesEnvironment parses env_files set for services to resolve the actual environment map for services
578+
// It returns a new Project instance with the changes and keep the original Project unchanged
579+
func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) (*Project, error) {
580+
newProject, err := p.deepCopy()
581+
if err != nil {
582+
return nil, err
583+
}
584+
for i, service := range newProject.Services {
585+
service.Environment = service.Environment.Resolve(newProject.Environment.Resolve)
540586

541587
environment := MappingWithEquals{}
542588
// resolve variables based on other files we already parsed, + project's environment
@@ -545,24 +591,24 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
545591
if ok && v != nil {
546592
return *v, ok
547593
}
548-
return p.Environment.Resolve(s)
594+
return newProject.Environment.Resolve(s)
549595
}
550596

551597
for _, envFile := range service.EnvFiles {
552598
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
553599
if envFile.Required {
554-
return errors.Wrapf(err, "env file %s not found", envFile.Path)
600+
return nil, errors.Wrapf(err, "env file %s not found", envFile.Path)
555601
}
556602
continue
557603
}
558604
b, err := os.ReadFile(envFile.Path)
559605
if err != nil {
560-
return errors.Wrapf(err, "failed to load %s", envFile.Path)
606+
return nil, errors.Wrapf(err, "failed to load %s", envFile.Path)
561607
}
562608

563609
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
564610
if err != nil {
565-
return errors.Wrapf(err, "failed to read %s", envFile.Path)
611+
return nil, errors.Wrapf(err, "failed to read %s", envFile.Path)
566612
}
567613
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
568614
}
@@ -572,7 +618,15 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
572618
if discardEnvFiles {
573619
service.EnvFiles = nil
574620
}
575-
p.Services[i] = service
621+
newProject.Services[i] = service
576622
}
577-
return nil
623+
return newProject, nil
624+
}
625+
626+
func (p *Project) deepCopy() (*Project, error) {
627+
instance, err := copystructure.Copy(p)
628+
if err != nil {
629+
return nil, err
630+
}
631+
return instance.(*Project), nil
578632
}

0 commit comments

Comments
 (0)