From 48a6dc088b1680d9c2b6c7f852259a33b5d3c338 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 20 Sep 2021 08:16:59 +0200 Subject: [PATCH] rebuild compose project from labels Signed-off-by: Nicolas De Loof --- pkg/api/labels.go | 2 ++ pkg/compose/create.go | 48 ++++++++++++++++++++++++------------- pkg/compose/down.go | 52 ++++++++++++++++------------------------ pkg/compose/down_test.go | 15 +++++++++--- pkg/compose/kill_test.go | 16 ++++++++----- pkg/compose/ps_test.go | 2 +- pkg/compose/stop_test.go | 6 ++--- 7 files changed, 80 insertions(+), 61 deletions(-) diff --git a/pkg/api/labels.go b/pkg/api/labels.go index a6cf51469..c0ed9ea7b 100644 --- a/pkg/api/labels.go +++ b/pkg/api/labels.go @@ -49,6 +49,8 @@ const ( SlugLabel = "com.docker.compose.slug" // ImageDigestLabel stores digest of the container image used to run service ImageDigestLabel = "com.docker.compose.image" + // DependenciesLabel stores service dependencies + DependenciesLabel = "com.docker.compose.depends_on" // VersionLabel stores the compose tool version used to run application VersionLabel = "com.docker.compose.version" ) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 60c319adf..9cd8f1018 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -226,27 +226,11 @@ func getImageName(service types.ServiceConfig, projectName string) string { func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig, number int, inherit *moby.Container, autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { - hash, err := ServiceHash(service) + labels, err := s.prepareLabels(p, service, number) if err != nil { return nil, nil, nil, err } - labels := map[string]string{} - for k, v := range service.Labels { - labels[k] = v - } - - labels[api.ProjectLabel] = p.Name - labels[api.ServiceLabel] = service.Name - labels[api.VersionLabel] = api.ComposeVersion - if _, ok := service.Labels[api.OneoffLabel]; !ok { - labels[api.OneoffLabel] = "False" - } - labels[api.ConfigHashLabel] = hash - labels[api.WorkingDirLabel] = p.WorkingDir - labels[api.ConfigFilesLabel] = strings.Join(p.ComposeFiles, ",") - labels[api.ContainerNumberLabel] = strconv.Itoa(number) - var ( runCmd strslice.StrSlice entrypoint strslice.StrSlice @@ -394,6 +378,36 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, return &containerConfig, &hostConfig, networkConfig, nil } +func (s *composeService) prepareLabels(p *types.Project, service types.ServiceConfig, number int) (map[string]string, error) { + labels := map[string]string{} + for k, v := range service.Labels { + labels[k] = v + } + + labels[api.ProjectLabel] = p.Name + labels[api.ServiceLabel] = service.Name + labels[api.VersionLabel] = api.ComposeVersion + if _, ok := service.Labels[api.OneoffLabel]; !ok { + labels[api.OneoffLabel] = "False" + } + + hash, err := ServiceHash(service) + if err != nil { + return nil, err + } + + labels[api.ConfigHashLabel] = hash + labels[api.WorkingDirLabel] = p.WorkingDir + labels[api.ConfigFilesLabel] = strings.Join(p.ComposeFiles, ",") + labels[api.ContainerNumberLabel] = strconv.Itoa(number) + var dependencies []string + for s := range service.DependsOn { + dependencies = append(dependencies, s) + } + labels[api.DependenciesLabel] = strings.Join(dependencies, ",") + return labels, nil +} + func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string { mode := "none" if len(project.Networks) > 0 { diff --git a/pkg/compose/down.go b/pkg/compose/down.go index c41bf245b..f3d6557e5 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -22,7 +22,6 @@ import ( "strings" "time" - "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -52,11 +51,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a } if options.Project == nil { - project, err := s.projectFromContainerLabels(containers, projectName) - if err != nil { - return err - } - options.Project = project + options.Project = s.projectFromContainerLabels(containers.filter(isNotOneOff), projectName) } if len(containers) > 0 { @@ -242,36 +237,31 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer return eg.Wait() } -func (s *composeService) projectFromContainerLabels(containers Containers, projectName string) (*types.Project, error) { - fakeProject := &types.Project{ +func (s *composeService) projectFromContainerLabels(containers Containers, projectName string) *types.Project { + project := &types.Project{ Name: projectName, } if len(containers) == 0 { - return fakeProject, nil + return project } - options, err := loadProjectOptionsFromLabels(containers[0]) - if err != nil { - return nil, err + set := map[string]moby.Container{} + for _, c := range containers { + set[c.Labels[api.ServiceLabel]] = c } - if options.ConfigPaths[0] == "-" { - for _, container := range containers { - fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{ - Name: container.Labels[api.ServiceLabel], - }) + for s, c := range set { + service := types.ServiceConfig{ + Name: s, + Image: c.Image, + Labels: c.Labels, } - return fakeProject, nil + dependencies := c.Labels[api.DependenciesLabel] + if len(dependencies) > 0 { + service.DependsOn = types.DependsOnConfig{} + for _, d := range strings.Split(dependencies, ",") { + service.DependsOn[d] = types.ServiceDependency{} + } + } + project.Services = append(project.Services, service) } - project, err := cli.ProjectFromOptions(options) - if err != nil { - return nil, err - } - - return project, nil -} - -func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) { - return cli.NewProjectOptions(strings.Split(c.Labels[api.ConfigFilesLabel], ","), - cli.WithOsEnv, - cli.WithWorkingDirectory(c.Labels[api.WorkingDirLabel]), - cli.WithName(c.Labels[api.ProjectLabel])) + return project } diff --git a/pkg/compose/down_test.go b/pkg/compose/down_test.go index 130acda58..bf6c9200a 100644 --- a/pkg/compose/down_test.go +++ b/pkg/compose/down_test.go @@ -38,7 +38,12 @@ func TestDown(t *testing.T) { tested.apiClient = api api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( - []moby.Container{testContainer("service1", "123"), testContainer("service2", "456"), testContainer("service2", "789"), testContainer("service_orphan", "321")}, nil) + []moby.Container{ + testContainer("service1", "123", false), + testContainer("service2", "456", false), + testContainer("service2", "789", false), + testContainer("service_orphan", "321", true), + }, nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "456", nil).Return(nil) @@ -64,7 +69,11 @@ func TestDownRemoveOrphans(t *testing.T) { tested.apiClient = api api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( - []moby.Container{testContainer("service1", "123"), testContainer("service2", "789"), testContainer("service_orphan", "321")}, nil) + []moby.Container{ + testContainer("service1", "123", false), + testContainer("service2", "789", false), + testContainer("service_orphan", "321", true), + }, nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil) @@ -90,7 +99,7 @@ func TestDownRemoveVolumes(t *testing.T) { tested.apiClient = api api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( - []moby.Container{testContainer("service1", "123")}, nil) + []moby.Container{testContainer("service1", "123", false)}, nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(nil) diff --git a/pkg/compose/kill_test.go b/pkg/compose/kill_test.go index 919caec6e..311421239 100644 --- a/pkg/compose/kill_test.go +++ b/pkg/compose/kill_test.go @@ -46,7 +46,7 @@ func TestKillAll(t *testing.T) { ctx := context.Background() api.EXPECT().ContainerList(ctx, projectFilterListOpt()).Return( - []moby.Container{testContainer("service1", "123"), testContainer("service1", "456"), testContainer("service2", "789")}, nil) + []moby.Container{testContainer("service1", "123", false), testContainer("service1", "456", false), testContainer("service2", "789", false)}, nil) api.EXPECT().ContainerKill(anyCancellableContext(), "123", "").Return(nil) api.EXPECT().ContainerKill(anyCancellableContext(), "456", "").Return(nil) api.EXPECT().ContainerKill(anyCancellableContext(), "789", "").Return(nil) @@ -64,7 +64,7 @@ func TestKillSignal(t *testing.T) { project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{testService("service1")}} ctx := context.Background() - api.EXPECT().ContainerList(ctx, projectFilterListOpt()).Return([]moby.Container{testContainer("service1", "123")}, nil) + api.EXPECT().ContainerList(ctx, projectFilterListOpt()).Return([]moby.Container{testContainer("service1", "123", false)}, nil) api.EXPECT().ContainerKill(anyCancellableContext(), "123", "SIGTERM").Return(nil) err := tested.kill(ctx, &project, compose.KillOptions{Signal: "SIGTERM"}) @@ -75,22 +75,26 @@ func testService(name string) types.ServiceConfig { return types.ServiceConfig{Name: name} } -func testContainer(service string, id string) moby.Container { +func testContainer(service string, id string, oneOff bool) moby.Container { return moby.Container{ ID: id, Names: []string{id}, - Labels: containerLabels(service), + Labels: containerLabels(service, oneOff), } } -func containerLabels(service string) map[string]string { +func containerLabels(service string, oneOff bool) map[string]string { workingdir, _ := filepath.Abs("testdata") composefile := filepath.Join(workingdir, "compose.yaml") - return map[string]string{ + labels := map[string]string{ compose.ServiceLabel: service, compose.ConfigFilesLabel: composefile, compose.WorkingDirLabel: workingdir, compose.ProjectLabel: strings.ToLower(testProject)} + if oneOff { + labels[compose.OneoffLabel] = "True" + } + return labels } func anyCancellableContext() gomock.Matcher { diff --git a/pkg/compose/ps_test.go b/pkg/compose/ps_test.go index 4b1128659..0644cc0c4 100644 --- a/pkg/compose/ps_test.go +++ b/pkg/compose/ps_test.go @@ -66,7 +66,7 @@ func containerDetails(service string, id string, status string, health string, e container := moby.Container{ ID: id, Names: []string{"/" + id}, - Labels: containerLabels(service), + Labels: containerLabels(service, false), State: status, } inspect := moby.ContainerJSON{ContainerJSONBase: &moby.ContainerJSONBase{State: &moby.ContainerState{Status: status, Health: &moby.Health{Status: health}, ExitCode: exitCode}}} diff --git a/pkg/compose/stop_test.go b/pkg/compose/stop_test.go index f38b1b607..4b8e345b6 100644 --- a/pkg/compose/stop_test.go +++ b/pkg/compose/stop_test.go @@ -40,9 +40,9 @@ func TestStopTimeout(t *testing.T) { ctx := context.Background() api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( []moby.Container{ - testContainer("service1", "123"), - testContainer("service1", "456"), - testContainer("service2", "789"), + testContainer("service1", "123", false), + testContainer("service1", "456", false), + testContainer("service2", "789", false), }, nil) timeout := time.Duration(2) * time.Second