Skip to content

Commit 59af3a9

Browse files
committed
Merge remote-tracking branch 'origin2/master' into composemaster
Signed-off-by: Yuval Kohavi <yuval.kohavi@gmail.com> Conflicts: tests/unit/service_test.py
2 parents 6cd3b00 + 80eaf4c commit 59af3a9

17 files changed

+844
-221
lines changed

compose/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
from __future__ import unicode_literals
2-
from .service import Service # noqa:flake8
32

43
__version__ = '1.3.0dev'

compose/cli/main.py

+30-21
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
import dockerpty
1212

1313
from .. import __version__
14+
from .. import migration
1415
from ..project import NoSuchService, ConfigurationError
15-
from ..service import BuildError, CannotBeScaledError
16+
from ..service import BuildError, CannotBeScaledError, NeedsBuildError
1617
from ..config import parse_environment
1718
from .command import Command
1819
from .docopt_command import NoSuchCommand
@@ -46,6 +47,9 @@ def main():
4647
except BuildError as e:
4748
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
4849
sys.exit(1)
50+
except NeedsBuildError as e:
51+
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
52+
sys.exit(1)
4953

5054

5155
def setup_logging():
@@ -81,20 +85,21 @@ class TopLevelCommand(Command):
8185
-v, --version Print version and exit
8286
8387
Commands:
84-
build Build or rebuild services
85-
help Get help on a command
86-
kill Kill containers
87-
logs View output from containers
88-
port Print the public port for a port binding
89-
ps List containers
90-
pull Pulls service images
91-
restart Restart services
92-
rm Remove stopped containers
93-
run Run a one-off command
94-
scale Set number of containers for a service
95-
start Start services
96-
stop Stop services
97-
up Create and start containers
88+
build Build or rebuild services
89+
help Get help on a command
90+
kill Kill containers
91+
logs View output from containers
92+
port Print the public port for a port binding
93+
ps List containers
94+
pull Pulls service images
95+
restart Restart services
96+
rm Remove stopped containers
97+
run Run a one-off command
98+
scale Set number of containers for a service
99+
start Start services
100+
stop Stop services
101+
up Create and start containers
102+
migrate_to_labels Recreate containers to add labels
98103
99104
"""
100105
def docopt_options(self):
@@ -295,9 +300,8 @@ def run(self, project, options):
295300
project.up(
296301
service_names=deps,
297302
start_deps=True,
298-
recreate=False,
303+
allow_recreate=False,
299304
insecure_registry=insecure_registry,
300-
detach=options['-d']
301305
)
302306

303307
tty = True
@@ -341,7 +345,6 @@ def run(self, project, options):
341345
service.start_container(container)
342346
print(container.name)
343347
else:
344-
service.start_container(container)
345348
dockerpty.start(project.client, container.id, interactive=not options['-T'])
346349
exit_code = container.wait()
347350
if options['--rm']:
@@ -440,6 +443,8 @@ def up(self, project, options):
440443
print new container names.
441444
--no-color Produce monochrome output.
442445
--no-deps Don't start linked services.
446+
--x-smart-recreate Only recreate containers whose configuration or
447+
image needs to be updated. (EXPERIMENTAL)
443448
--no-recreate If containers already exist, don't recreate them.
444449
--no-build Don't build an image, even if it's missing
445450
-t, --timeout TIMEOUT When attached, use this timeout in seconds
@@ -452,15 +457,16 @@ def up(self, project, options):
452457
monochrome = options['--no-color']
453458

454459
start_deps = not options['--no-deps']
455-
recreate = not options['--no-recreate']
460+
allow_recreate = not options['--no-recreate']
461+
smart_recreate = options['--x-smart-recreate']
456462
service_names = options['SERVICE']
457463

458464
project.up(
459465
service_names=service_names,
460466
start_deps=start_deps,
461-
recreate=recreate,
467+
allow_recreate=allow_recreate,
468+
smart_recreate=smart_recreate,
462469
insecure_registry=insecure_registry,
463-
detach=detached,
464470
do_build=not options['--no-build'],
465471
)
466472

@@ -483,6 +489,9 @@ def handler(signal, frame):
483489
params = {} if timeout is None else {'timeout': int(timeout)}
484490
project.stop(service_names=service_names, **params)
485491

492+
def migrate_to_labels(self, project, _options):
493+
migration.migrate_project_to_labels(project)
494+
486495

487496
def list_containers(containers):
488497
return ", ".join(c.name for c in containers)

compose/const.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
3+
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
4+
LABEL_PROJECT = 'com.docker.compose.project'
5+
LABEL_SERVICE = 'com.docker.compose.service'
6+
LABEL_VERSION = 'com.docker.compose.version'
7+
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'

compose/container.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import six
55
from functools import reduce
66

7+
from .const import LABEL_CONTAINER_NUMBER, LABEL_SERVICE
8+
79

810
class Container(object):
911
"""
@@ -58,14 +60,15 @@ def name(self):
5860

5961
@property
6062
def name_without_project(self):
61-
return '_'.join(self.dictionary['Name'].split('_')[1:])
63+
return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number)
6264

6365
@property
6466
def number(self):
65-
try:
66-
return int(self.name.split('_')[-1])
67-
except ValueError:
68-
return None
67+
number = self.labels.get(LABEL_CONTAINER_NUMBER)
68+
if not number:
69+
raise ValueError("Container {0} does not have a {1} label".format(
70+
self.short_id, LABEL_CONTAINER_NUMBER))
71+
return int(number)
6972

7073
@property
7174
def ports(self):
@@ -159,6 +162,7 @@ def inspect(self):
159162
self.has_been_inspected = True
160163
return self.dictionary
161164

165+
# TODO: only used by tests, move to test module
162166
def links(self):
163167
links = []
164168
for container in self.client.containers():
@@ -175,13 +179,16 @@ def attach_socket(self, **kwargs):
175179
return self.client.attach_socket(self.id, **kwargs)
176180

177181
def __repr__(self):
178-
return '<Container: %s>' % self.name
182+
return '<Container: %s (%s)>' % (self.name, self.id[:6])
179183

180184
def __eq__(self, other):
181185
if type(self) != type(other):
182186
return False
183187
return self.id == other.id
184188

189+
def __hash__(self):
190+
return self.id.__hash__()
191+
185192

186193
def get_container_name(container):
187194
if not container.get('Name') and not container.get('Names'):

compose/migration.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import logging
2+
import re
3+
4+
from .container import get_container_name, Container
5+
6+
7+
log = logging.getLogger(__name__)
8+
9+
10+
# TODO: remove this section when migrate_project_to_labels is removed
11+
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
12+
13+
14+
def is_valid_name(name):
15+
match = NAME_RE.match(name)
16+
return match is not None
17+
18+
19+
def add_labels(project, container, name):
20+
project_name, service_name, one_off, number = NAME_RE.match(name).groups()
21+
if project_name != project.name or service_name not in project.service_names:
22+
return
23+
service = project.get_service(service_name)
24+
service.recreate_container(container)
25+
26+
27+
def migrate_project_to_labels(project):
28+
log.info("Running migration to labels for project %s", project.name)
29+
30+
client = project.client
31+
for container in client.containers(all=True):
32+
name = get_container_name(container)
33+
if not is_valid_name(name):
34+
continue
35+
add_labels(project, Container.from_ps(client, container), name)

compose/project.py

+80-25
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import unicode_literals
22
from __future__ import absolute_import
33
import logging
4-
54
from functools import reduce
5+
6+
from docker.errors import APIError
7+
68
from .config import get_service_name_from_net, ConfigurationError
7-
from .service import Service
9+
from .const import LABEL_PROJECT, LABEL_ONE_OFF
10+
from .service import Service, check_for_legacy_containers
811
from .container import Container
9-
from docker.errors import APIError
1012

1113
log = logging.getLogger(__name__)
1214

@@ -60,6 +62,12 @@ def __init__(self, name, services, client):
6062
self.services = services
6163
self.client = client
6264

65+
def labels(self, one_off=False):
66+
return [
67+
'{0}={1}'.format(LABEL_PROJECT, self.name),
68+
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
69+
]
70+
6371
@classmethod
6472
def from_dicts(cls, name, service_dicts, client):
6573
"""
@@ -75,6 +83,10 @@ def from_dicts(cls, name, service_dicts, client):
7583
volumes_from=volumes_from, **service_dict))
7684
return project
7785

86+
@property
87+
def service_names(self):
88+
return [service.name for service in self.services]
89+
7890
def get_service(self, name):
7991
"""
8092
Retrieve a service by name. Raises NoSuchService
@@ -102,7 +114,7 @@ def get_services(self, service_names=None, include_deps=False):
102114
"""
103115
if service_names is None or len(service_names) == 0:
104116
return self.get_services(
105-
service_names=[s.name for s in self.services],
117+
service_names=self.service_names,
106118
include_deps=include_deps
107119
)
108120
else:
@@ -195,24 +207,59 @@ def build(self, service_names=None, no_cache=False):
195207
def up(self,
196208
service_names=None,
197209
start_deps=True,
198-
recreate=True,
210+
allow_recreate=True,
211+
smart_recreate=False,
199212
insecure_registry=False,
200-
detach=False,
201213
do_build=True):
202-
running_containers = []
203-
for service in self.get_services(service_names, include_deps=start_deps):
204-
if recreate:
205-
create_func = service.recreate_containers
214+
215+
services = self.get_services(service_names, include_deps=start_deps)
216+
217+
plans = self._get_convergence_plans(
218+
services,
219+
allow_recreate=allow_recreate,
220+
smart_recreate=smart_recreate,
221+
)
222+
223+
return [
224+
container
225+
for service in services
226+
for container in service.execute_convergence_plan(
227+
plans[service.name],
228+
insecure_registry=insecure_registry,
229+
do_build=do_build,
230+
)
231+
]
232+
233+
def _get_convergence_plans(self,
234+
services,
235+
allow_recreate=True,
236+
smart_recreate=False):
237+
238+
plans = {}
239+
240+
for service in services:
241+
updated_dependencies = [
242+
name
243+
for name in service.get_dependency_names()
244+
if name in plans
245+
and plans[name].action == 'recreate'
246+
]
247+
248+
if updated_dependencies:
249+
log.debug(
250+
'%s has not changed but its dependencies (%s) have, so recreating',
251+
service.name, ", ".join(updated_dependencies),
252+
)
253+
plan = service.recreate_plan()
206254
else:
207-
create_func = service.start_or_create_containers
255+
plan = service.convergence_plan(
256+
allow_recreate=allow_recreate,
257+
smart_recreate=smart_recreate,
258+
)
208259

209-
for container in create_func(
210-
insecure_registry=insecure_registry,
211-
detach=detach,
212-
do_build=do_build):
213-
running_containers.append(container)
260+
plans[service.name] = plan
214261

215-
return running_containers
262+
return plans
216263

217264
def pull(self, service_names=None, insecure_registry=False):
218265
for service in self.get_services(service_names, include_deps=True):
@@ -223,16 +270,24 @@ def remove_stopped(self, service_names=None, **options):
223270
service.remove_stopped(**options)
224271

225272
def containers(self, service_names=None, stopped=False, one_off=False):
226-
return [Container.from_ps(self.client, container)
227-
for container in self.client.containers(all=stopped)
228-
for service in self.get_services(service_names)
229-
if service.has_container(container, one_off=one_off)]
273+
containers = [
274+
Container.from_ps(self.client, container)
275+
for container in self.client.containers(
276+
all=stopped,
277+
filters={'label': self.labels(one_off=one_off)})]
278+
279+
if not containers:
280+
check_for_legacy_containers(
281+
self.client,
282+
self.name,
283+
self.service_names,
284+
stopped=stopped,
285+
one_off=one_off)
286+
287+
return containers
230288

231289
def _inject_deps(self, acc, service):
232-
net_name = service.get_net_name()
233-
dep_names = (service.get_linked_names() +
234-
service.get_volumes_from_names() +
235-
([net_name] if net_name else []))
290+
dep_names = service.get_dependency_names()
236291

237292
if len(dep_names) > 0:
238293
dep_services = self.get_services(

0 commit comments

Comments
 (0)