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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown

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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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