Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..const import IS_WINDOWS_PLATFORM
from ..progress_stream import StreamOutputError
from ..project import NoSuchService
from ..project import OneOffFilter
from ..service import BuildAction
from ..service import BuildError
from ..service import ConvergenceStrategy
Expand Down Expand Up @@ -437,7 +438,7 @@ def ps(self, options):
"""
containers = sorted(
self.project.containers(service_names=options['SERVICE'], stopped=True) +
self.project.containers(service_names=options['SERVICE'], one_off=True),
self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
key=attrgetter('name'))

if options['-q']:
Expand Down Expand Up @@ -491,8 +492,14 @@ def rm(self, options):
Options:
-f, --force Don't ask to confirm removal
-v Remove volumes associated with containers
-a, --all Also remove one-off containers created by
docker-compose run
"""
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
one_off = OneOffFilter.include if options.get('--all') else OneOffFilter.exclude

all_containers = self.project.containers(
service_names=options['SERVICE'], stopped=True, one_off=one_off
)
stopped_containers = [c for c in all_containers if not c.is_running]

if len(stopped_containers) > 0:
Expand All @@ -501,7 +508,8 @@ def rm(self, options):
or yesno("Are you sure? [yN] ", default=False):
self.project.remove_stopped(
service_names=options['SERVICE'],
v=options.get('-v', False)
v=options.get('-v', False),
one_off=one_off
)
else:
print("No stopped containers")
Expand Down
29 changes: 23 additions & 6 deletions compose/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import operator
from functools import reduce

import enum
from docker.errors import APIError

from . import parallel
Expand Down Expand Up @@ -35,6 +36,20 @@
log = logging.getLogger(__name__)


@enum.unique
class OneOffFilter(enum.Enum):
include = 0
exclude = 1
only = 2

@classmethod
def update_labels(cls, value, labels):
if value == cls.only:
labels.append('{0}={1}'.format(LABEL_ONE_OFF, "True"))
elif value == cls.exclude or value is False:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why value is False here? I think it might be safer to change the existing calls.

labels.append('{0}={1}'.format(LABEL_ONE_OFF, "False"))


class Project(object):
"""
A collection of services.
Expand All @@ -47,10 +62,10 @@ def __init__(self, name, services, client, networks=None, volumes=None):
self.networks = networks or ProjectNetworks({}, False)

def labels(self, one_off=False):
return [
'{0}={1}'.format(LABEL_PROJECT, self.name),
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
]
labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)]

OneOffFilter.update_labels(one_off, labels)
return labels

@classmethod
def from_config(cls, name, config_data, client):
Expand Down Expand Up @@ -249,8 +264,10 @@ def unpause(self, service_names=None, **options):
def kill(self, service_names=None, **options):
parallel.parallel_kill(self.containers(service_names), options)

def remove_stopped(self, service_names=None, **options):
parallel.parallel_remove(self.containers(service_names, stopped=True), options)
def remove_stopped(self, service_names=None, one_off=False, **options):
parallel.parallel_remove(self.containers(
service_names, stopped=True, one_off=one_off
), options)

def down(self, remove_image_type, include_volumes, remove_orphans=False):
self.stop()
Expand Down
1 change: 1 addition & 0 deletions docs/reference/rm.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Usage: rm [options] [SERVICE...]
Options:
-f, --force Don't ask to confirm removal
-v Remove volumes associated with containers
-a, --all Also remove one-off containers
```

Removes stopped service containers.
Expand Down
55 changes: 39 additions & 16 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .. import mock
from compose.cli.command import get_project
from compose.container import Container
from compose.project import OneOffFilter
from tests.integration.testcases import DockerClientTestCase
from tests.integration.testcases import get_links
from tests.integration.testcases import pull_busybox
Expand Down Expand Up @@ -105,7 +106,7 @@ def tearDown(self):
self.project.kill()
self.project.remove_stopped()

for container in self.project.containers(stopped=True, one_off=True):
for container in self.project.containers(stopped=True, one_off=OneOffFilter.only):
container.remove(force=True)

networks = self.client.networks()
Expand Down Expand Up @@ -802,7 +803,7 @@ def test_run_service_without_links(self):
self.assertEqual(len(self.project.containers()), 0)

# Ensure stdin/out was open
container = self.project.containers(stopped=True, one_off=True)[0]
container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
config = container.inspect()['Config']
self.assertTrue(config['AttachStderr'])
self.assertTrue(config['AttachStdout'])
Expand Down Expand Up @@ -852,15 +853,15 @@ def test_run_without_command(self):

self.dispatch(['run', 'implicit'])
service = self.project.get_service('implicit')
containers = service.containers(stopped=True, one_off=True)
containers = service.containers(stopped=True, one_off=OneOffFilter.only)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/sh -c echo "success"'],
)

self.dispatch(['run', 'explicit'])
service = self.project.get_service('explicit')
containers = service.containers(stopped=True, one_off=True)
containers = service.containers(stopped=True, one_off=OneOffFilter.only)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/true'],
Expand All @@ -871,7 +872,7 @@ def test_run_service_with_entrypoint_overridden(self):
name = 'service'
self.dispatch(['run', '--entrypoint', '/bin/echo', name, 'helloworld'])
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
self.assertEqual(
shlex.split(container.human_readable_command),
[u'/bin/echo', u'helloworld'],
Expand All @@ -883,7 +884,7 @@ def test_run_service_with_user_overridden(self):
user = 'sshd'
self.dispatch(['run', '--user={user}'.format(user=user), name], returncode=1)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
self.assertEqual(user, container.get('Config.User'))

def test_run_service_with_user_overridden_short_form(self):
Expand All @@ -892,7 +893,7 @@ def test_run_service_with_user_overridden_short_form(self):
user = 'sshd'
self.dispatch(['run', '-u', user, name], returncode=1)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
self.assertEqual(user, container.get('Config.User'))

def test_run_service_with_environement_overridden(self):
Expand All @@ -906,7 +907,7 @@ def test_run_service_with_environement_overridden(self):
'/bin/true',
])
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
# env overriden
self.assertEqual('notbar', container.environment['foo'])
# keep environement from yaml
Expand All @@ -920,7 +921,7 @@ def test_run_service_without_map_ports(self):
# create one off container
self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch(['run', '-d', 'simple'])
container = self.project.get_service('simple').containers(one_off=True)[0]
container = self.project.get_service('simple').containers(one_off=OneOffFilter.only)[0]

# get port information
port_random = container.get_local_port(3000)
Expand All @@ -937,7 +938,7 @@ def test_run_service_with_map_ports(self):
# create one off container
self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch(['run', '-d', '--service-ports', 'simple'])
container = self.project.get_service('simple').containers(one_off=True)[0]
container = self.project.get_service('simple').containers(one_off=OneOffFilter.only)[0]

# get port information
port_random = container.get_local_port(3000)
Expand All @@ -958,7 +959,7 @@ def test_run_service_with_explicitly_maped_ports(self):
# create one off container
self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple'])
container = self.project.get_service('simple').containers(one_off=True)[0]
container = self.project.get_service('simple').containers(one_off=OneOffFilter.only)[0]

# get port information
port_short = container.get_local_port(3000)
Expand All @@ -980,7 +981,7 @@ def test_run_service_with_explicitly_maped_ip_ports(self):
'--publish', '127.0.0.1:30001:3001',
'simple'
])
container = self.project.get_service('simple').containers(one_off=True)[0]
container = self.project.get_service('simple').containers(one_off=OneOffFilter.only)[0]

# get port information
port_short = container.get_local_port(3000)
Expand All @@ -997,7 +998,7 @@ def test_run_with_expose_ports(self):
# create one off container
self.base_dir = 'tests/fixtures/expose-composefile'
self.dispatch(['run', '-d', '--service-ports', 'simple'])
container = self.project.get_service('simple').containers(one_off=True)[0]
container = self.project.get_service('simple').containers(one_off=OneOffFilter.only)[0]

ports = container.ports
self.assertEqual(len(ports), 9)
Expand All @@ -1021,7 +1022,7 @@ def test_run_with_custom_name(self):
self.dispatch(['run', '--name', name, 'service', '/bin/true'])

service = self.project.get_service('service')
container, = service.containers(stopped=True, one_off=True)
container, = service.containers(stopped=True, one_off=OneOffFilter.only)
self.assertEqual(container.name, name)

def test_run_service_with_workdir_overridden(self):
Expand Down Expand Up @@ -1051,7 +1052,7 @@ def test_run_interactive_connects_to_network(self):
self.dispatch(['run', 'app', 'nslookup', 'db'])

containers = self.project.get_service('app').containers(
stopped=True, one_off=True)
stopped=True, one_off=OneOffFilter.only)
assert len(containers) == 2

for container in containers:
Expand All @@ -1071,7 +1072,7 @@ def test_run_detached_connects_to_network(self):
self.dispatch(['up', '-d'])
self.dispatch(['run', '-d', 'app', 'top'])

container = self.project.get_service('app').containers(one_off=True)[0]
container = self.project.get_service('app').containers(one_off=OneOffFilter.only)[0]
networks = container.get('NetworkSettings.Networks')

assert sorted(list(networks)) == [
Expand Down Expand Up @@ -1125,6 +1126,28 @@ def test_rm(self):
self.dispatch(['rm', '-f'], None)
self.assertEqual(len(service.containers(stopped=True)), 0)

def test_rm_all(self):
service = self.project.get_service('simple')
service.create_container(one_off=False)
service.create_container(one_off=True)
kill_service(service)
self.assertEqual(len(service.containers(stopped=True)), 1)
self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 1)
self.dispatch(['rm', '-f'], None)
self.assertEqual(len(service.containers(stopped=True)), 0)
self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 1)
self.dispatch(['rm', '-f', '-a'], None)
self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 0)

service.create_container(one_off=False)
service.create_container(one_off=True)
kill_service(service)
self.assertEqual(len(service.containers(stopped=True)), 1)
self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 1)
self.dispatch(['rm', '-f', '--all'], None)
self.assertEqual(len(service.containers(stopped=True)), 0)
self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 0)

def test_stop(self):
self.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
Expand Down
5 changes: 3 additions & 2 deletions tests/integration/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from compose.const import LABEL_SERVICE
from compose.const import LABEL_VERSION
from compose.container import Container
from compose.project import OneOffFilter
from compose.service import ConvergencePlan
from compose.service import ConvergenceStrategy
from compose.service import NetworkMode
Expand Down Expand Up @@ -60,7 +61,7 @@ def test_containers_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
self.assertEqual(db.containers(stopped=True), [])
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
self.assertEqual(db.containers(one_off=OneOffFilter.only, stopped=True), [container])

def test_project_is_added_to_container_name(self):
service = self.create_service('web')
Expand Down Expand Up @@ -494,7 +495,7 @@ def test_start_one_off_container_creates_links_to_its_own_service(self):
create_and_start_container(db)
create_and_start_container(db)

c = create_and_start_container(db, one_off=True)
c = create_and_start_container(db, one_off=OneOffFilter.only)

self.assertEqual(
set(get_links(c)),
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from compose.const import LABEL_PROJECT
from compose.const import LABEL_SERVICE
from compose.container import Container
from compose.project import OneOffFilter
from compose.service import build_ulimits
from compose.service import build_volume_binding
from compose.service import BuildAction
Expand Down Expand Up @@ -256,7 +257,7 @@ def test_get_container_create_options_with_name_option(self):
opts = service._get_container_create_options(
{'name': name},
1,
one_off=True)
one_off=OneOffFilter.only)
self.assertEqual(opts['name'], name)

def test_get_container_create_options_does_not_mutate_options(self):
Expand Down