diff --git a/compose/cli/main.py b/compose/cli/main.py index 2fb8536497d..96ed062b7db 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -13,7 +13,7 @@ from .. import __version__ from ..project import NoSuchService, ConfigurationError from ..service import BuildError, CannotBeScaledError -from ..config import parse_environment +from ..config import parse_environment, load from .command import Command from .docopt_command import NoSuchCommand from .errors import UserError @@ -270,19 +270,29 @@ def run(self, project, options): Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] Options: - --allow-insecure-ssl Allow insecure connections to the docker - registry - -d Detached mode: Run container in the background, print - new container name. - --entrypoint CMD Override the entrypoint of the image. - -e KEY=VAL Set an environment variable (can be used multiple times) - -u, --user="" Run as specified username or uid - --no-deps Don't start linked services. - --rm Remove container after run. Ignored in detached mode. - --service-ports Run command with the service's ports enabled and mapped - to the host. - -T Disable pseudo-tty allocation. By default `docker-compose run` - allocates a TTY. + --allow-insecure-ssl Allow insecure connections to the docker + registry + -d Detached mode: Run container in the background, + print new container name. + --entrypoint CMD Override the entrypoint of the image. + -e KEY=VAL Set an environment variable (can be used multiple + times) + --labels-file FILE Uses all labels, in the given yaml FILE for the + SERVICE to be run. + --no-deps Don't start linked services. + --no-prefix-labels Don't prefix '.cfg' and '.cmd' when --labels-file + is used. + --remove-labels [REGEX] Removes all labels. If REGEX is given, removes + labels that match the given REGular EXpression. + (Note: This option has less priority than the + given --labels-file) + --rm Remove container after run. Ignored in detached + mode. + --service-ports Run command with the service's ports enabled and + mapped to the host. + -T Disable pseudo-tty allocation. By default + `docker-compose run` allocates a TTY. + -u, --user="" Run as specified username or uid. """ service = project.get_service(options['SERVICE']) @@ -314,8 +324,42 @@ def run(self, project, options): 'tty': tty, 'stdin_open': not options['-d'], 'detach': options['-d'], + 'labels': dict(), } + if options['--remove-labels']: + service_cfg_lbls = service.options.get('labels') + if service_cfg_lbls is not None: + regex = options.get('--remove-labels') + a = re.compile(regex) + service.options['labels'] = dict( + (k, v) for (k,v) in service_cfg_lbls.iteritems() + if not a.match(k)) + + if options['--labels-file']: + # Merge labels from config with labels from --labels-file + label_cfg_file = self.get_config_path(options.get('--labels-file')) + label_list_cfg_file = load(label_cfg_file) + service_cmd_opts = [a_serv_lbl for a_serv_lbl in label_list_cfg_file + if a_serv_lbl.get('name') == service.name + and a_serv_lbl.get('labels') is not None] + if len(service_cmd_opts) == 0: + raise UserError('No labels defined for service \'' + service.name + + '\' in ' + options.get('--labels-file')) + service_cmd_lbls = service_cmd_opts[0]['labels'] + service_cfg_lbls = service.options.get('labels') + prefix_cmd = "" + prefix_cfg = "" + if not options['--no-prefix-labels']: + prefix_cmd = ".cmd:" + prefix_cfg = ".cfg:" + + if service_cfg_lbls is not None: + container_options['labels'].update( + (prefix_cfg + key, service_cfg_lbls[key]) for key in service_cfg_lbls) + container_options['labels'].update( + (prefix_cmd + key, service_cmd_lbls[key]) for key in service_cmd_lbls) + if options['-e']: # Merge environment from config with -e command line container_options['environment'] = dict( @@ -434,16 +478,23 @@ def up(self, project, options): Usage: up [options] [SERVICE...] Options: - --allow-insecure-ssl Allow insecure connections to the docker - registry - -d Detached mode: Run containers in the background, - print new container names. - --no-color Produce monochrome output. - --no-deps Don't start linked services. - --no-recreate If containers already exist, don't recreate them. - --no-build Don't build an image, even if it's missing - -t, --timeout TIMEOUT When attached, use this timeout in seconds - for the shutdown. (default: 10) + --allow-insecure-ssl Allow insecure connections to the docker registry. + -d Detached mode: Run containers in the background, + print new container names. + --labels-file FILE Uses all labels, in the given yaml FILE for the + SERVICEs to be up. + --no-build Don't build an image, even if it's missing. + --no-deps Don't start linked services. + --no-color Produce monochrome output. + --no-prefix-labels Don't prefix '.cfg' and '.cmd' when --labels-file + is used. + --no-recreate If containers already exist, don't recreate them. + --remove-labels [REGEX] Removes all labels. If REGEX is given, removes + labels that match the given REGular EXpression. + (Note: This option has less priority than the + given --labels-file). + -t, --timeout TIMEOUT When attached, use this timeout in seconds for the + shutdown. (default: 10) """ insecure_registry = options['--allow-insecure-ssl'] @@ -451,6 +502,11 @@ def up(self, project, options): monochrome = options['--no-color'] + if options['--labels-file']: + labels_file = self.get_config_path(options['--labels-file']) + else: + labels_file = '' + start_deps = not options['--no-deps'] recreate = not options['--no-recreate'] service_names = options['SERVICE'] @@ -462,6 +518,9 @@ def up(self, project, options): insecure_registry=insecure_registry, detach=detached, do_build=not options['--no-build'], + prefix_labels=not options['--no-prefix-labels'], + remove_labels=str(options['--remove-labels']), + labels_file=labels_file ) to_attach = [c for s in project.get_services(service_names) for c in s.containers()] diff --git a/compose/config.py b/compose/config.py index a4e3a991f76..5aba5be13de 100644 --- a/compose/config.py +++ b/compose/config.py @@ -17,6 +17,7 @@ 'environment', 'hostname', 'image', + 'labels', 'links', 'mem_limit', 'net', @@ -138,7 +139,6 @@ def resolve_environment(service_dict, working_dir=None): service_dict['environment'] = env return service_dict - def parse_environment(environment): if not environment: return {} @@ -154,7 +154,6 @@ def parse_environment(environment): environment ) - def split_env(env): if '=' in env: return env.split('=', 1) diff --git a/compose/project.py b/compose/project.py index 881d8eb0ac5..1c4fa3325ee 100644 --- a/compose/project.py +++ b/compose/project.py @@ -209,20 +209,30 @@ def up(self, recreate=True, insecure_registry=False, detach=False, - do_build=True): + do_build=True, + prefix_labels=False, + remove_labels='', + labels_file=None + ): running_containers = [] for service in self.get_services(service_names, include_deps=start_deps): if recreate: for (_, container) in service.recreate_containers( insecure_registry=insecure_registry, detach=detach, - do_build=do_build): + do_build=do_build, + prefix_labels=prefix_labels, + remove_labels=remove_labels, + labels_file=labels_file): running_containers.append(container) else: for container in service.start_or_create_containers( insecure_registry=insecure_registry, detach=detach, - do_build=do_build): + do_build=do_build, + prefix_labels=prefix_labels, + remove_labels=remove_labels, + labels_file=labels_file): running_containers.append(container) return running_containers diff --git a/compose/service.py b/compose/service.py index c65874c26a6..162b6264f06 100644 --- a/compose/service.py +++ b/compose/service.py @@ -9,7 +9,7 @@ from docker.errors import APIError -from .config import DOCKER_CONFIG_KEYS +from .config import DOCKER_CONFIG_KEYS, load from .container import Container, get_container_name from .progress_stream import stream_output, StreamOutputError @@ -29,6 +29,7 @@ VALID_NAME_CHARS = '[a-zA-Z0-9]' +LABEL_PREFIX = '' class BuildError(Exception): def __init__(self, service, reason): @@ -50,6 +51,9 @@ class ConfigError(ValueError): ServiceName = namedtuple('ServiceName', 'project service number') +LabelSpec = namedtuple('LabelSpec', 'key value') + + class Service(object): def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=None, **options): if not re.match('^%s+$' % VALID_NAME_CHARS, name): @@ -181,7 +185,6 @@ def create_container(self, self.can_be_built() and not self.client.images(name=self.full_name)): self.build() - try: return Container.create(self.client, **container_options) except APIError as e: @@ -296,7 +299,8 @@ def start_or_create_containers( self, insecure_registry=False, detach=False, - do_build=True): + do_build=True, + **override_options): containers = self.containers(stopped=True) if not containers: @@ -305,6 +309,7 @@ def start_or_create_containers( insecure_registry=insecure_registry, detach=detach, do_build=do_build, + **override_options ) return [self.start_container(new_container)] else: @@ -399,6 +404,51 @@ def _get_container_create_options(self, override_options, one_off=False): self.containers(stopped=True, one_off=one_off), one_off) + if 'labels' in container_options: + container_options['labels'] = dict( + (LABEL_PREFIX + key, container_options['labels'][key]) + for key in container_options['labels']) + + if 'remove_labels' in container_options: + service_cfg_lbls = container_options.get('labels') + if service_cfg_lbls is not None: + regex = container_options.get('remove_labels') + a = re.compile(regex) + container_options['labels'] = dict( + (k, v) for (k,v) in service_cfg_lbls.iteritems() + if not a.match(k)) + del container_options['remove_labels'] + + if 'labels_file' in container_options: + # Merge labels from config with labels from --labels-file + label_cfg_file = container_options.get('labels_file') + label_list_cfg_file = load(label_cfg_file) + service_cmd_opts = [a_serv_lbl for a_serv_lbl in label_list_cfg_file + if a_serv_lbl.get('name') == self.name + and a_serv_lbl.get('labels') is not None] + if len(service_cmd_opts) == 0: + raise ConfigError ('No labels defined for service \'' + + self.name + '\' in ' + + container_options.get('labels_file')) + service_cmd_lbls = service_cmd_opts[0]['labels'] + service_cfg_lbls = container_options.get('labels') + prefix_cmd = "" + prefix_cfg = "" + if 'prefix_labels' in container_options: + prefix_cmd = ".cmd:" + prefix_cfg = ".cfg:" + del container_options['prefix_labels'] + temp_labels = dict() + if service_cfg_lbls is not None: + temp_labels.update( + (prefix_cfg + key, service_cfg_lbls[key]) for key in service_cfg_lbls) + temp_labels.update( + (prefix_cmd + key, service_cmd_lbls[key]) for key in service_cmd_lbls) + + container_options['labels'] = temp_labels + del container_options['labels_file'] + + # If a qualified hostname was given, split it into an # unqualified hostname and a domainname unless domainname # was also given explicitly. This matches the behavior of @@ -553,7 +603,6 @@ def parse_volume_spec(volume_config): return VolumeSpec(external, internal, mode) - def parse_repository_tag(s): if ":" not in s: return s, ""