Skip to content

Commit d700d4c

Browse files
committed
Improve projects support
1 parent 577797d commit d700d4c

17 files changed

Lines changed: 348 additions & 94 deletions

config/config.yml

Whitespace-only changes.

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: '3'
21
services:
32
lazydocker:
43
build:

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var (
2929
configFlag = false
3030
debuggingFlag = false
3131
composeFiles []string
32+
projectName string
3233
)
3334

3435
func main() {
@@ -51,6 +52,7 @@ func main() {
5152
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
5253
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
5354
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
55+
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
5456
flaggy.SetVersion(info)
5557

5658
flaggy.Parse()
@@ -71,7 +73,7 @@ func main() {
7173
log.Fatal(err.Error())
7274
}
7375

74-
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir)
76+
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
7577
if err != nil {
7678
log.Fatal(err.Error())
7779
}

pkg/cheatsheet/generate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func Generate() {
3333
}
3434

3535
func generateAtDir(dir string) {
36-
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "")
36+
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
3737
if err != nil {
3838
panic(err)
3939
}

pkg/commands/docker.go

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
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
6772
func (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+
221341
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
222342
L:
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'
368489
func (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 {

pkg/commands/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
type Service struct {
1414
Name string
1515
ID string
16+
ProjectName string
1617
OSCommand *OSCommand
1718
Log *logrus.Entry
1819
Container *Container

pkg/config/app_config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,11 @@ type AppConfig struct {
488488
UserConfig *UserConfig
489489
ConfigDir string
490490
ProjectDir string
491+
ProjectName string
491492
}
492493

493494
// NewAppConfig makes a new app config
494-
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) {
495+
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
495496
configDir, err := findOrCreateConfigDir(name)
496497
if err != nil {
497498
return nil, err
@@ -517,6 +518,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
517518
UserConfig: userConfig,
518519
ConfigDir: configDir,
519520
ProjectDir: projectDir,
521+
ProjectName: projectName,
520522
}
521523

522524
return appConfig, nil

pkg/config/app_config_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
func TestDockerComposeCommandNoFiles(t *testing.T) {
1111
composeFiles := []string{}
12-
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
12+
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
1313
if err != nil {
1414
t.Fatalf("Unexpected error: %s", err)
1515
}
@@ -23,7 +23,7 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
2323

2424
func TestDockerComposeCommandSingleFile(t *testing.T) {
2525
composeFiles := []string{"one.yml"}
26-
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
26+
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
2727
if err != nil {
2828
t.Fatalf("Unexpected error: %s", err)
2929
}
@@ -37,7 +37,7 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
3737

3838
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
3939
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
40-
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
40+
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
4141
if err != nil {
4242
t.Fatalf("Unexpected error: %s", err)
4343
}
@@ -52,7 +52,7 @@ func TestDockerComposeCommandMultipleFiles(t *testing.T) {
5252
func TestWritingToConfigFile(t *testing.T) {
5353
// init the AppConfig
5454
emptyComposeFiles := []string{}
55-
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir")
55+
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
5656
if err != nil {
5757
t.Fatalf("Unexpected error: %s", err)
5858
}

pkg/gui/arrangement.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,21 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
175175
return defaultBox
176176
}
177177

178-
return append([]*boxlayout.Box{
179-
{
178+
// The project panel is compact (Size: 3) when not focused, but expands
179+
// when focused to show the list of projects.
180+
projectBox := &boxlayout.Box{
181+
Window: sideWindowNames[0],
182+
Size: 3,
183+
}
184+
if currentWindow == sideWindowNames[0] {
185+
projectBox = &boxlayout.Box{
180186
Window: sideWindowNames[0],
181-
Size: 3,
182-
},
187+
Weight: 2,
188+
}
189+
}
190+
191+
return append([]*boxlayout.Box{
192+
projectBox,
183193
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
184194
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
185195
})...)

0 commit comments

Comments
 (0)