@@ -27,13 +27,16 @@ import (
27
27
"github.com/compose-spec/compose-go/v2/dotenv"
28
28
"github.com/compose-spec/compose-go/v2/utils"
29
29
"github.com/distribution/reference"
30
+ "github.com/mitchellh/copystructure"
30
31
godigest "github.com/opencontainers/go-digest"
31
32
"github.com/pkg/errors"
32
33
"golang.org/x/sync/errgroup"
33
34
"gopkg.in/yaml.v3"
34
35
)
35
36
36
37
// 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.
37
40
type Project struct {
38
41
Name string `yaml:"name,omitempty" json:"name,omitempty"`
39
42
WorkingDir string `yaml:"-" json:"-"`
@@ -185,13 +188,19 @@ func (p *Project) AllServices() Services {
185
188
186
189
type ServiceFunc func (name string , service ServiceConfig ) error
187
190
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
+ }
190
198
if len (options ) == 0 {
191
199
// backward compatibility
192
200
options = []DependencyOption {IncludeDependencies }
193
201
}
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
195
204
}
196
205
197
206
type withServicesOptions struct {
@@ -291,53 +300,69 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
291
300
}
292
301
293
302
// 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
+ }
295
309
for _ , p := range profiles {
296
310
if p == "*" {
297
- return
311
+ return newProject , nil
298
312
}
299
313
}
300
314
enabled := Services {}
301
315
disabled := Services {}
302
- for name , service := range p .AllServices () {
316
+ for name , service := range newProject .AllServices () {
303
317
if service .HasProfile (profiles ) {
304
318
enabled [name ] = service
305
319
} else {
306
320
disabled [name ] = service
307
321
}
308
322
}
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
312
327
}
313
328
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
+ }
316
336
if len (names ) == 0 {
317
- return nil
337
+ return newProject , nil
318
338
}
319
339
320
340
profiles := append ([]string {}, p .Profiles ... )
321
341
for _ , name := range names {
322
- if _ , ok := p .Services [name ]; ok {
342
+ if _ , ok := newProject .Services [name ]; ok {
323
343
// already enabled
324
344
continue
325
345
}
326
346
service := p .DisabledServices [name ]
327
347
profiles = append (profiles , service .Profiles ... )
328
348
}
329
- p .ApplyProfiles (profiles )
349
+ newProject , err = newProject .ApplyProfiles (profiles )
350
+ if err != nil {
351
+ return newProject , err
352
+ }
330
353
331
- return p .ResolveServicesEnvironment (true )
354
+ return newProject .ResolveServicesEnvironment (true )
332
355
}
333
356
334
357
// 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 ()
336
361
requiredNetworks := map [string ]struct {}{}
337
362
requiredVolumes := map [string ]struct {}{}
338
363
requiredSecrets := map [string ]struct {}{}
339
364
requiredConfigs := map [string ]struct {}{}
340
- for _ , s := range p .Services {
365
+ for _ , s := range newProject .Services {
341
366
for k := range s .Networks {
342
367
requiredNetworks [k ] = struct {}{}
343
368
}
@@ -366,31 +391,32 @@ func (p *Project) WithoutUnnecessaryResources() {
366
391
networks [k ] = value
367
392
}
368
393
}
369
- p .Networks = networks
394
+ newProject .Networks = networks
370
395
371
396
volumes := Volumes {}
372
397
for k := range requiredVolumes {
373
398
if value , ok := p .Volumes [k ]; ok {
374
399
volumes [k ] = value
375
400
}
376
401
}
377
- p .Volumes = volumes
402
+ newProject .Volumes = volumes
378
403
379
404
secrets := Secrets {}
380
405
for k := range requiredSecrets {
381
406
if value , ok := p .Secrets [k ]; ok {
382
407
secrets [k ] = value
383
408
}
384
409
}
385
- p .Secrets = secrets
410
+ newProject .Secrets = secrets
386
411
387
412
configs := Configs {}
388
413
for k := range requiredConfigs {
389
414
if value , ok := p .Configs [k ]; ok {
390
415
configs [k ] = value
391
416
}
392
417
}
393
- p .Configs = configs
418
+ newProject .Configs = configs
419
+ return newProject , err
394
420
}
395
421
396
422
type DependencyOption func (options * withServicesOptions )
@@ -407,25 +433,26 @@ func IgnoreDependencies(options *withServicesOptions) {
407
433
options .dependencyPolicy = ignoreDependencies
408
434
}
409
435
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 ) {
412
439
if len (names ) == 0 {
413
440
// All services
414
- return nil
441
+ return p . deepCopy ()
415
442
}
416
443
417
444
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 {
419
446
set .Add (name )
420
447
return nil
421
448
}, options ... )
422
449
if err != nil {
423
- return err
450
+ return nil , err
424
451
}
425
452
426
453
// Disable all services which are not explicit target or dependencies
427
454
enabled := Services {}
428
- for name , s := range p .Services {
455
+ for name , s := range newProject .Services {
429
456
if _ , ok := set [name ]; ok {
430
457
// remove all dependencies but those implied by explicitly selected services
431
458
dependencies := s .DependsOn
@@ -437,32 +464,46 @@ func (p *Project) ForServices(names []string, options ...DependencyOption) error
437
464
s .DependsOn = dependencies
438
465
enabled [name ] = s
439
466
} else {
440
- p .DisableService (s )
467
+ if newProject , err = newProject .DisableService (s ); err != nil {
468
+ return nil , err
469
+ }
441
470
}
442
471
}
443
- p .Services = enabled
444
- return nil
472
+ newProject .Services = enabled
473
+ return newProject , nil
445
474
}
446
475
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
+ }
448
483
// We should remove all dependencies which reference the disabled service
449
- for i , s := range p .Services {
484
+ for i , s := range newProject .Services {
450
485
if _ , ok := s .DependsOn [service .Name ]; ok {
451
486
delete (s .DependsOn , service .Name )
452
- p .Services [i ] = s
487
+ newProject .Services [i ] = s
453
488
}
454
489
}
455
490
delete (p .Services , service .Name )
456
- if p .DisabledServices == nil {
457
- p .DisabledServices = Services {}
491
+ if newProject .DisabledServices == nil {
492
+ newProject .DisabledServices = Services {}
458
493
}
459
- p .DisabledServices [service .Name ] = service
494
+ newProject .DisabledServices [service .Name ] = service
495
+ return newProject , err
460
496
}
461
497
462
498
// 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
+ }
464
505
eg := errgroup.Group {}
465
- for i , s := range p .Services {
506
+ for i , s := range newProject .Services {
466
507
idx := i
467
508
service := s
468
509
@@ -488,11 +529,11 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D
488
529
}
489
530
490
531
service .Image = named .String ()
491
- p .Services [idx ] = service
532
+ newProject .Services [idx ] = service
492
533
return nil
493
534
})
494
535
}
495
- return eg .Wait ()
536
+ return newProject , eg .Wait ()
496
537
}
497
538
498
539
// MarshalYAML marshal Project into a yaml tree
@@ -533,10 +574,15 @@ func (p *Project) MarshalJSON() ([]byte, error) {
533
574
return json .Marshal (m )
534
575
}
535
576
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 )
540
586
541
587
environment := MappingWithEquals {}
542
588
// resolve variables based on other files we already parsed, + project's environment
@@ -545,24 +591,24 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
545
591
if ok && v != nil {
546
592
return * v , ok
547
593
}
548
- return p .Environment .Resolve (s )
594
+ return newProject .Environment .Resolve (s )
549
595
}
550
596
551
597
for _ , envFile := range service .EnvFiles {
552
598
if _ , err := os .Stat (envFile .Path ); os .IsNotExist (err ) {
553
599
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 )
555
601
}
556
602
continue
557
603
}
558
604
b , err := os .ReadFile (envFile .Path )
559
605
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 )
561
607
}
562
608
563
609
fileVars , err := dotenv .ParseWithLookup (bytes .NewBuffer (b ), resolve )
564
610
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 )
566
612
}
567
613
environment .OverrideBy (Mapping (fileVars ).ToMappingWithEquals ())
568
614
}
@@ -572,7 +618,15 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
572
618
if discardEnvFiles {
573
619
service .EnvFiles = nil
574
620
}
575
- p .Services [i ] = service
621
+ newProject .Services [i ] = service
576
622
}
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
578
632
}
0 commit comments