@@ -87,82 +87,116 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
8787
8888 prepareNetworks (project )
8989
90- networks , err := s .ensureNetworks (ctx , project )
90+ // Temporary implementation of use_api_socket until we get actual support inside docker engine
91+ project , err = s .useAPISocket (project )
9192 if err != nil {
9293 return err
9394 }
9495
95- volumes , err := s .ensureProjectVolumes (ctx , project )
96+ // Phase 1: Inspect current state
97+ observed , err := s .InspectState (ctx , project )
9698 if err != nil {
9799 return err
98100 }
99101
100- var observedState Containers
101- observedState , err = s .getContainers (ctx , project .Name , oneOffInclude , true )
102- if err != nil {
103- return err
104- }
105- orphans := observedState .filter (isOrphaned (project ))
106- if len (orphans ) > 0 && ! options .IgnoreOrphans {
107- if options .RemoveOrphans {
108- err := s .removeContainers (ctx , orphans , nil , nil , false )
109- if err != nil {
110- return err
111- }
112- } else {
102+ // Handle orphan containers
103+ if len (observed .Orphans ) > 0 && ! options .IgnoreOrphans {
104+ if ! options .RemoveOrphans {
113105 logrus .Warnf ("Found orphan containers (%s) for this project. If " +
114106 "you removed or renamed this service in your compose " +
115107 "file, you can run this command with the " +
116- "--remove-orphans flag to clean it up." , orphans .names ())
108+ "--remove-orphans flag to clean it up." , observed . Orphans .names ())
117109 }
118110 }
119111
120- // Temporary implementation of use_api_socket until we get actual support inside docker engine
121- project , err = s .useAPISocket (project )
122- if err != nil {
112+ // Validate external networks exist before reconciling
113+ if err := s .validateExternalNetworks (ctx , project , options .Services ); err != nil {
123114 return err
124115 }
125116
126- return newConvergence (options .Services , observedState , networks , volumes , s ).apply (ctx , project , options )
127- }
117+ // Phase 2: Reconcile desired vs observed state (pure function)
118+ plan , err := Reconcile (project , observed , ReconcileOptions {
119+ Recreate : options .Recreate ,
120+ RecreateDependencies : options .RecreateDependencies ,
121+ Services : options .Services ,
122+ Inherit : options .Inherit ,
123+ Timeout : options .Timeout ,
124+ RemoveOrphans : options .RemoveOrphans ,
125+ })
126+ if err != nil {
127+ return err
128+ }
128129
129- func prepareNetworks (project * types.Project ) {
130- for k , nw := range project .Networks {
131- nw .CustomLabels = nw .CustomLabels .
132- Add (api .NetworkLabel , k ).
133- Add (api .ProjectLabel , project .Name ).
134- Add (api .VersionLabel , api .ComposeVersion )
135- project .Networks [k ] = nw
130+ if plan .IsEmpty () {
131+ return nil
136132 }
133+
134+ s .emitUntouchedContainerEvents (project , observed , plan )
135+
136+ // Phase 3: Execute the plan
137+ return s .ExecutePlan (ctx , project , plan )
137138}
138139
139- func (s * composeService ) ensureNetworks (ctx context.Context , project * types.Project ) (map [string ]string , error ) {
140- networks := map [string ]string {}
141- for name , nw := range project .Networks {
142- id , err := s .ensureNetwork (ctx , project , name , & nw )
143- if err != nil {
144- return nil , err
140+ // emitUntouchedContainerEvents emits progress events for containers that are
141+ // already up-to-date and running, so that callers (e.g. scale) can see them.
142+ func (s * composeService ) emitUntouchedContainerEvents (project * types.Project , observed * ObservedState , plan * ReconciliationPlan ) {
143+ for _ , service := range project .Services {
144+ for _ , ctr := range observed .Containers [service .Name ] {
145+ ctrName := getCanonicalContainerName (ctr )
146+ if _ , touched := plan .Operations ["recreate-container:" + ctrName ]; touched {
147+ continue
148+ }
149+ if _ , touched := plan .Operations ["stop-container:" + ctrName ]; touched {
150+ continue
151+ }
152+ if _ , touched := plan .Operations ["create-container:" + ctrName ]; touched {
153+ continue
154+ }
155+ if ctr .State == container .StateRunning {
156+ s .events .On (runningEvent (getContainerProgressName (ctr )))
157+ }
145158 }
146- networks [name ] = id
147- project .Networks [name ] = nw
148159 }
149- return networks , nil
150160}
151161
152- func (s * composeService ) ensureProjectVolumes (ctx context.Context , project * types.Project ) (map [string ]string , error ) {
153- ids := map [string ]string {}
154- for k , volume := range project .Volumes {
155- volume .CustomLabels = volume .CustomLabels .Add (api .VolumeLabel , k )
156- volume .CustomLabels = volume .CustomLabels .Add (api .ProjectLabel , project .Name )
157- volume .CustomLabels = volume .CustomLabels .Add (api .VersionLabel , api .ComposeVersion )
158- id , err := s .ensureVolume (ctx , k , volume , project )
162+ // validateExternalNetworks checks that external networks exist for services
163+ // that are part of the current operation. Returns an error if a required
164+ // external network is not found.
165+ func (s * composeService ) validateExternalNetworks (ctx context.Context , project * types.Project , services []string ) error {
166+ for key , net := range project .Networks {
167+ if ! net .External {
168+ continue
169+ }
170+ // Check if any targeted service uses this network
171+ usedByTargetedService := false
172+ for _ , service := range project .Services {
173+ if len (services ) > 0 && ! slices .Contains (services , service .Name ) {
174+ continue
175+ }
176+ if _ , ok := service .Networks [key ]; ok {
177+ usedByTargetedService = true
178+ break
179+ }
180+ }
181+ if ! usedByTargetedService {
182+ continue
183+ }
184+ _ , err := s .resolveExternalNetwork (ctx , & net )
159185 if err != nil {
160- return nil , err
186+ return err
161187 }
162- ids [k ] = id
163188 }
189+ return nil
190+ }
164191
165- return ids , nil
192+ func prepareNetworks (project * types.Project ) {
193+ for k , nw := range project .Networks {
194+ nw .CustomLabels = nw .CustomLabels .
195+ Add (api .NetworkLabel , k ).
196+ Add (api .ProjectLabel , project .Name ).
197+ Add (api .VersionLabel , api .ComposeVersion )
198+ project .Networks [k ] = nw
199+ }
166200}
167201
168202//nolint:gocyclo
0 commit comments