@@ -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.
3740type 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
186189type 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
197206type 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
396422type 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