Skip to content

Commit 2a198d4

Browse files
authored
Merge pull request #685 from fhke/feature/compose-up-idempotent-remove-orhans
Enhancements for `compose up` command.
2 parents 9017fc0 + fe8bd45 commit 2a198d4

File tree

8 files changed

+214
-21
lines changed

8 files changed

+214
-21
lines changed

cmd/nerdctl/compose_up.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func newComposeUpCommand() *cobra.Command {
4141
composeUpCommand.Flags().Bool("build", false, "Build images before starting containers.")
4242
composeUpCommand.Flags().Bool("ipfs", false, "Allow pulling base images from IPFS during build")
4343
composeUpCommand.Flags().Bool("quiet-pull", false, "Pull without printing progress information")
44+
composeUpCommand.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.")
4445
composeUpCommand.Flags().StringArray("scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
4546
return composeUpCommand
4647
}
@@ -77,6 +78,10 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
7778
if err != nil {
7879
return err
7980
}
81+
removeOrphans, err := cmd.Flags().GetBool("remove-orphans")
82+
if err != nil {
83+
return err
84+
}
8085
scaleSlice, err := cmd.Flags().GetStringArray("scale")
8186
if err != nil {
8287
return err
@@ -105,14 +110,15 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
105110
return err
106111
}
107112
uo := composer.UpOptions{
108-
Detach: detach,
109-
NoBuild: noBuild,
110-
NoColor: noColor,
111-
NoLogPrefix: noLogPrefix,
112-
ForceBuild: build,
113-
IPFS: enableIPFS,
114-
QuietPull: quietPull,
115-
Scale: scale,
113+
Detach: detach,
114+
NoBuild: noBuild,
115+
NoColor: noColor,
116+
NoLogPrefix: noLogPrefix,
117+
ForceBuild: build,
118+
IPFS: enableIPFS,
119+
QuietPull: quietPull,
120+
RemoveOrphans: removeOrphans,
121+
Scale: scale,
116122
}
117123
return c.Up(ctx, uo, services)
118124
}

cmd/nerdctl/compose_up_linux_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,67 @@ networks:
329329

330330
base.Cmd("inspect", "-f", `{{json .NetworkSettings.Networks }}`, projectName+"_foo_1").AssertOutContains("10.1.100.")
331331
}
332+
333+
func TestComposeUpRemoveOrphans(t *testing.T) {
334+
base := testutil.NewBase(t)
335+
336+
var (
337+
dockerComposeYAMLOrphan = fmt.Sprintf(`
338+
version: '3.1'
339+
340+
services:
341+
test:
342+
image: %s
343+
command: "sleep infinity"
344+
`, testutil.AlpineImage)
345+
346+
dockerComposeYAMLFull = fmt.Sprintf(`
347+
%s
348+
orphan:
349+
image: %s
350+
command: "sleep infinity"
351+
`, dockerComposeYAMLOrphan, testutil.AlpineImage)
352+
)
353+
354+
compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan)
355+
defer compOrphan.CleanUp()
356+
compFull := testutil.NewComposeDir(t, dockerComposeYAMLFull)
357+
defer compFull.CleanUp()
358+
359+
projectName := fmt.Sprintf("nerdctl-compose-test-%d", time.Now().Unix())
360+
t.Logf("projectName=%q", projectName)
361+
362+
orphanContainer := fmt.Sprintf("%s_orphan_1", projectName)
363+
364+
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK()
365+
defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run()
366+
base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d").AssertOK()
367+
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutContains(orphanContainer)
368+
base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d", "--remove-orphans").AssertOK()
369+
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutNotContains(orphanContainer)
370+
}
371+
372+
func TestComposeUpIdempotent(t *testing.T) {
373+
base := testutil.NewBase(t)
374+
375+
var dockerComposeYAML = fmt.Sprintf(`
376+
version: '3.1'
377+
378+
services:
379+
test:
380+
image: %s
381+
command: "sleep infinity"
382+
`, testutil.AlpineImage)
383+
384+
comp := testutil.NewComposeDir(t, dockerComposeYAML)
385+
defer comp.CleanUp()
386+
387+
projectName := comp.ProjectName()
388+
t.Logf("projectName=%q", projectName)
389+
390+
base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
391+
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
392+
base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
393+
base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK()
394+
395+
}

pkg/composer/composer.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"path/filepath"
2727

2828
composecli "github.com/compose-spec/compose-go/cli"
29-
"github.com/compose-spec/compose-go/types"
3029
compose "github.com/compose-spec/compose-go/types"
3130
"github.com/containerd/containerd"
3231
"github.com/containerd/containerd/identifiers"
@@ -172,7 +171,7 @@ func findComposeYAML(o *Options) (string, error) {
172171

173172
func (c *Composer) Services(ctx context.Context) ([]*serviceparser.Service, error) {
174173
var services []*serviceparser.Service
175-
if err := c.project.WithServices(nil, func(svc types.ServiceConfig) error {
174+
if err := c.project.WithServices(nil, func(svc compose.ServiceConfig) error {
176175
parsed, err := serviceparser.Parse(c.project, svc)
177176
if err != nil {
178177
return err
@@ -187,7 +186,7 @@ func (c *Composer) Services(ctx context.Context) ([]*serviceparser.Service, erro
187186

188187
func (c *Composer) ServiceNames(services ...string) ([]string, error) {
189188
var names []string
190-
if err := c.project.WithServices(services, func(svc types.ServiceConfig) error {
189+
if err := c.project.WithServices(services, func(svc compose.ServiceConfig) error {
191190
names = append(names, svc.Name)
192191
return nil
193192
}); err != nil {

pkg/composer/container.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,24 @@ func (c *Composer) Containers(ctx context.Context, services ...string) ([]contai
4141
}
4242
return containers, nil
4343
}
44+
45+
func (c *Composer) containerExists(ctx context.Context, name, service string) (bool, error) {
46+
// get list of containers for service
47+
containers, err := c.Containers(ctx, service)
48+
if err != nil {
49+
return false, err
50+
}
51+
52+
for _, container := range containers {
53+
containerLabels, err := container.Labels(ctx)
54+
if err != nil {
55+
return false, err
56+
}
57+
if name == containerLabels[labels.Name] {
58+
// container exists
59+
return true, nil
60+
}
61+
}
62+
// container doesn't exist
63+
return false, nil
64+
}

pkg/composer/orphans.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package composer
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/containerd/containerd"
24+
"github.com/containerd/nerdctl/pkg/composer/serviceparser"
25+
"github.com/containerd/nerdctl/pkg/labels"
26+
)
27+
28+
func (c *Composer) getOrphanContainers(ctx context.Context, parsedServices []*serviceparser.Service) ([]containerd.Container, error) {
29+
// get all running containers for project
30+
var filters = []string{fmt.Sprintf("labels.%q==%s", labels.ComposeProject, c.project.Name)}
31+
containers, err := c.client.Containers(ctx, filters...)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
var orphanedContainers []containerd.Container
37+
38+
outer:
39+
for _, container := range containers {
40+
containerLabels, err := container.Labels(ctx)
41+
if err != nil {
42+
return nil, fmt.Errorf("error getting container labels: %s", err)
43+
}
44+
containerName := containerLabels[labels.Name]
45+
46+
for _, parsedService := range parsedServices {
47+
for _, serviceContainer := range parsedService.Containers {
48+
if containerName == serviceContainer.Name {
49+
// container name exists in parsedServices
50+
continue outer
51+
}
52+
}
53+
}
54+
// container name does not exist in parsedServices
55+
orphanedContainers = append(orphanedContainers, container)
56+
}
57+
58+
return orphanedContainers, nil
59+
}

pkg/composer/up.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ import (
2929
)
3030

3131
type UpOptions struct {
32-
Detach bool
33-
NoBuild bool
34-
NoColor bool
35-
NoLogPrefix bool
36-
ForceBuild bool
37-
IPFS bool
38-
QuietPull bool
39-
Scale map[string]uint64 // map of service name to replicas
32+
Detach bool
33+
NoBuild bool
34+
NoColor bool
35+
NoLogPrefix bool
36+
ForceBuild bool
37+
IPFS bool
38+
QuietPull bool
39+
RemoveOrphans bool
40+
Scale map[string]uint64 // map of service name to replicas
4041
}
4142

4243
func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) error {
@@ -86,7 +87,24 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro
8687
return err
8788
}
8889

89-
return c.upServices(ctx, parsedServices, uo)
90+
if err := c.upServices(ctx, parsedServices, uo); err != nil {
91+
return err
92+
}
93+
94+
if uo.RemoveOrphans {
95+
orphans, err := c.getOrphanContainers(ctx, parsedServices)
96+
if err != nil {
97+
return fmt.Errorf("error getting orphaned containers: %s", err)
98+
}
99+
if len(orphans) == 0 {
100+
return nil
101+
}
102+
if err := c.downContainers(ctx, orphans, true); err != nil {
103+
return fmt.Errorf("error removing orphaned containers: %s", err)
104+
}
105+
}
106+
107+
return nil
90108
}
91109

92110
func validateFileObjectConfig(obj types.FileObjectConfig, shortName, objType string, project *types.Project) error {

pkg/composer/up_service.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,23 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser
128128
// upServiceContainer must be called after ensureServiceImage
129129
// upServiceContainer returns container ID
130130
func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container) (string, error) {
131-
logrus.Infof("Creating container %s", container.Name)
131+
// check if container already exists
132+
exists, err := c.containerExists(ctx, container.Name, service.Unparsed.Name)
133+
if err != nil {
134+
return "", fmt.Errorf("error while checking for containers with name %q: %s", container.Name, err)
135+
}
136+
137+
// delete container if it already exists
138+
if exists {
139+
logrus.Debugf("Container %q already exists, deleting", container.Name)
140+
delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name)
141+
if err = delCmd.Run(); err != nil {
142+
return "", fmt.Errorf("could not delete container %q: %s", container.Name, err)
143+
}
144+
logrus.Infof("Re-creating container %s", container.Name)
145+
} else {
146+
logrus.Infof("Creating container %s", container.Name)
147+
}
132148

133149
//add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels
134150
container.RunArgs = append([]string{

pkg/testutil/testutil.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ func (c *Cmd) AssertOutContains(s string) {
290290
c.Assert(expected)
291291
}
292292

293+
func (c *Cmd) AssertOutNotContains(s string) {
294+
c.AssertOutWithFunc(func(stdout string) error {
295+
if strings.Contains(stdout, s) {
296+
return fmt.Errorf("expected stdout to contain %q", s)
297+
} else {
298+
return nil
299+
}
300+
})
301+
}
302+
293303
func (c *Cmd) AssertOutExactly(s string) {
294304
c.Base.T.Helper()
295305
fn := func(stdout string) error {

0 commit comments

Comments
 (0)