99 ogLog "log"
1010 "os"
1111 "os/exec"
12+ "path"
13+ "sort"
1214 "strings"
1315 "sync"
1416 "time"
@@ -39,9 +41,11 @@ type DockerCommand struct {
3941 Config * config.AppConfig
4042 Client * client.Client
4143 InDockerComposeProject bool
42- ErrorChan chan error
43- ContainerMutex deadlock.Mutex
44- ServiceMutex deadlock.Mutex
44+ // LocalProjectName is the compose project name for the directory where lazydocker was launched.
45+ LocalProjectName string
46+ ErrorChan chan error
47+ ContainerMutex deadlock.Mutex
48+ ServiceMutex deadlock.Mutex
4549
4650 Closers []io.Closer
4751}
@@ -61,12 +65,22 @@ type CommandObject struct {
6165 Image * Image
6266 Volume * Volume
6367 Network * Network
68+ Project * Project
6469}
6570
6671// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
6772func (c * DockerCommand ) NewCommandObject (obj CommandObject ) CommandObject {
6873 defaultObj := CommandObject {DockerCompose : c .Config .UserConfig .CommandTemplates .DockerCompose }
6974 _ = mergo .Merge (& defaultObj , obj )
75+
76+ // When operating on a specific project, include -p flag so that
77+ // docker compose targets the correct project.
78+ if obj .Service != nil && obj .Service .ProjectName != "" {
79+ defaultObj .DockerCompose = fmt .Sprintf ("%s -p %s" , defaultObj .DockerCompose , obj .Service .ProjectName )
80+ } else if obj .Project != nil && obj .Project .Name != "" {
81+ defaultObj .DockerCompose = fmt .Sprintf ("%s -p %s" , defaultObj .DockerCompose , obj .Project .Name )
82+ }
83+
7084 return defaultObj
7185}
7286
@@ -193,7 +207,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
193207 container .MonitoringStats = false
194208}
195209
196- func (c * DockerCommand ) RefreshContainersAndServices (currentServices [] * Service , currentContainers []* Container ) ([]* Container , []* Service , error ) {
210+ func (c * DockerCommand ) RefreshContainersAndServices (currentContainers []* Container ) ([]* Container , []* Service , error ) {
197211 c .ServiceMutex .Lock ()
198212 defer c .ServiceMutex .Unlock ()
199213
@@ -202,27 +216,133 @@ func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service,
202216 return nil , nil , err
203217 }
204218
205- var services []* Service
206- // we only need to get these services once because they won't change in the runtime of the program
207- if currentServices != nil {
208- services = currentServices
209- } else {
210- services , err = c .GetServices ()
211- if err != nil {
212- return nil , nil , err
219+ // Derive services from container labels (covers all projects)
220+ services := c .GetServicesFromContainers (containers )
221+
222+ var composeServices []* Service
223+ if c .InDockerComposeProject {
224+ composeServices , _ = c .GetServices ()
225+ }
226+
227+ // Determine the local project name before merging services, since
228+ // mergeServices needs it. We match compose service names against container
229+ // labels to handle cases where the project name differs from the directory
230+ // name (e.g. a `name:` directive in the compose file).
231+ if c .LocalProjectName == "" && c .InDockerComposeProject && composeServices != nil {
232+ for _ , ctr := range containers {
233+ if ctr .ProjectName == "" || ctr .ServiceName == "" {
234+ continue
235+ }
236+ for _ , svc := range composeServices {
237+ if ctr .ServiceName == svc .Name {
238+ c .LocalProjectName = ctr .ProjectName
239+ break
240+ }
241+ }
242+ if c .LocalProjectName != "" {
243+ break
244+ }
245+ }
246+ // Fall back to directory name
247+ if c .LocalProjectName == "" && c .Config .ProjectDir != "" {
248+ c .LocalProjectName = path .Base (c .Config .ProjectDir )
213249 }
214250 }
215251
252+ // Merge compose services (which include stopped services) with
253+ // container-derived services from all projects
254+ if composeServices != nil {
255+ services = c .mergeServices (services , composeServices )
256+ }
257+
216258 c .assignContainersToServices (containers , services )
217259
218260 return containers , services , nil
219261}
220262
263+ // GetServicesFromContainers derives services from container labels for all projects
264+ func (c * DockerCommand ) GetServicesFromContainers (containers []* Container ) []* Service {
265+ // Use project+service as key to avoid duplicates
266+ type serviceKey struct {
267+ project string
268+ service string
269+ }
270+ seen := make (map [serviceKey ]bool )
271+ var services []* Service
272+
273+ for _ , ctr := range containers {
274+ if ctr .ServiceName == "" || ctr .OneOff {
275+ continue
276+ }
277+ key := serviceKey {project : ctr .ProjectName , service : ctr .ServiceName }
278+ if seen [key ] {
279+ continue
280+ }
281+ seen [key ] = true
282+ services = append (services , & Service {
283+ Name : ctr .ServiceName ,
284+ ID : ctr .ServiceName ,
285+ ProjectName : ctr .ProjectName ,
286+ OSCommand : c .OSCommand ,
287+ Log : c .Log ,
288+ DockerCommand : c ,
289+ })
290+ }
291+
292+ return services
293+ }
294+
295+ // mergeServices merges compose services (which may lack ProjectName) with
296+ // container-derived services. Compose services take priority because they
297+ // include services without running containers.
298+ func (c * DockerCommand ) mergeServices (containerServices []* Service , composeServices []* Service ) []* Service {
299+ // Set project name on compose services
300+ for _ , svc := range composeServices {
301+ if svc .ProjectName == "" {
302+ svc .ProjectName = c .LocalProjectName
303+ }
304+ }
305+
306+ // Build a set of compose service names for the local project
307+ composeServiceNames := make (map [string ]bool )
308+ for _ , svc := range composeServices {
309+ composeServiceNames [svc .Name ] = true
310+ }
311+
312+ // Start with compose services, then add container-derived services
313+ // that aren't already covered by compose (i.e. from other projects)
314+ result := make ([]* Service , 0 , len (composeServices )+ len (containerServices ))
315+ result = append (result , composeServices ... )
316+
317+ for _ , svc := range containerServices {
318+ if svc .ProjectName == c .LocalProjectName && composeServiceNames [svc .Name ] {
319+ continue // already covered by compose service
320+ }
321+ result = append (result , svc )
322+ }
323+
324+ return result
325+ }
326+
327+ // GetProjectNames returns all unique project names from containers
328+ func (c * DockerCommand ) GetProjectNames (containers []* Container ) []string {
329+ seen := make (map [string ]bool )
330+ var names []string
331+ for _ , ctr := range containers {
332+ if ctr .ProjectName != "" && ! seen [ctr .ProjectName ] {
333+ seen [ctr .ProjectName ] = true
334+ names = append (names , ctr .ProjectName )
335+ }
336+ }
337+ sort .Strings (names )
338+ return names
339+ }
340+
221341func (c * DockerCommand ) assignContainersToServices (containers []* Container , services []* Service ) {
222342L:
223343 for _ , service := range services {
224344 for _ , ctr := range containers {
225- if ! ctr .OneOff && ctr .ServiceName == service .Name {
345+ if ! ctr .OneOff && ctr .ServiceName == service .Name && ctr . ProjectName == service . ProjectName {
226346 service .Container = ctr
227347 continue L
228348 }
@@ -312,6 +432,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
312432 services [i ] = & Service {
313433 Name : str ,
314434 ID : str ,
435+ ProjectName : c .LocalProjectName ,
315436 OSCommand : c .OSCommand ,
316437 Log : c .Log ,
317438 DockerCommand : c ,
@@ -351,11 +472,11 @@ func (c *DockerCommand) SetContainerDetails(containers []*Container) {
351472}
352473
353474// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
354- func (c * DockerCommand ) ViewAllLogs () (* exec.Cmd , error ) {
475+ func (c * DockerCommand ) ViewAllLogs (project * Project ) (* exec.Cmd , error ) {
355476 cmd := c .OSCommand .ExecutableFromString (
356477 utils .ApplyTemplate (
357478 c .OSCommand .Config .UserConfig .CommandTemplates .ViewAllLogs ,
358- c .NewCommandObject (CommandObject {}),
479+ c .NewCommandObject (CommandObject {Project : project }),
359480 ),
360481 )
361482
@@ -366,10 +487,15 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
366487
367488// DockerComposeConfig returns the result of 'docker-compose config'
368489func (c * DockerCommand ) DockerComposeConfig () string {
490+ return c .DockerComposeConfigForProject (nil )
491+ }
492+
493+ // DockerComposeConfigForProject returns the result of 'docker-compose config' for a specific project
494+ func (c * DockerCommand ) DockerComposeConfigForProject (project * Project ) string {
369495 output , err := c .OSCommand .RunCommandWithOutput (
370496 utils .ApplyTemplate (
371497 c .OSCommand .Config .UserConfig .CommandTemplates .DockerComposeConfig ,
372- c .NewCommandObject (CommandObject {}),
498+ c .NewCommandObject (CommandObject {Project : project }),
373499 ),
374500 )
375501 if err != nil {
0 commit comments