Extract YAML loading and parsing into config module

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
Aanand Prasad 2015-02-27 11:54:57 +00:00
commit 4ecf5e01ff
11 changed files with 358 additions and 284 deletions

View file

@ -4,9 +4,9 @@ from requests.exceptions import ConnectionError, SSLError
import logging
import os
import re
import yaml
import six
from .. import config
from ..project import Project
from ..service import ConfigError
from .docopt_command import DocoptCommand
@ -69,18 +69,11 @@ class Command(DocoptCommand):
return verbose_proxy.VerboseProxy('docker', client)
return client
def get_config(self, config_path):
try:
with open(config_path, 'r') as fh:
return yaml.safe_load(fh)
except IOError as e:
raise errors.UserError(six.text_type(e))
def get_project(self, config_path, project_name=None, verbose=False):
try:
return Project.from_config(
return Project.from_dicts(
self.get_project_name(config_path, project_name),
self.get_config(config_path),
config.load(config_path),
self.get_client(verbose=verbose))
except ConfigError as e:
raise errors.UserError(six.text_type(e))

View file

@ -12,7 +12,8 @@ import dockerpty
from .. import __version__
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError, parse_environment
from ..service import BuildError, CannotBeScaledError
from ..config import parse_environment
from .command import Command
from .docopt_command import NoSuchCommand
from .errors import UserError

180
compose/config.py Normal file
View file

@ -0,0 +1,180 @@
import os
import yaml
import six
DOCKER_CONFIG_KEYS = [
'cap_add',
'cap_drop',
'cpu_shares',
'command',
'detach',
'dns',
'dns_search',
'domainname',
'entrypoint',
'env_file',
'environment',
'hostname',
'image',
'links',
'mem_limit',
'net',
'ports',
'privileged',
'restart',
'stdin_open',
'tty',
'user',
'volumes',
'volumes_from',
'working_dir',
]
ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'build',
'expose',
'external_links',
'name',
]
DOCKER_CONFIG_HINTS = {
'cpu_share' : 'cpu_shares',
'link' : 'links',
'port' : 'ports',
'privilege' : 'privileged',
'priviliged': 'privileged',
'privilige' : 'privileged',
'volume' : 'volumes',
'workdir' : 'working_dir',
}
def load(filename):
return from_dictionary(load_yaml(filename))
def load_yaml(filename):
try:
with open(filename, 'r') as fh:
return yaml.safe_load(fh)
except IOError as e:
raise ConfigurationError(six.text_type(e))
def from_dictionary(dictionary):
service_dicts = []
for service_name, service_dict in list(dictionary.items()):
if not isinstance(service_dict, dict):
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name)
service_dict = make_service_dict(service_name, service_dict)
service_dicts.append(service_dict)
return service_dicts
def make_service_dict(name, options):
service_dict = options.copy()
service_dict['name'] = name
return process_container_options(service_dict)
def process_container_options(service_dict):
for k in service_dict:
if k not in ALLOWED_KEYS:
msg = "Unsupported config option for %s service: '%s'" % (service_dict['name'], k)
if k in DOCKER_CONFIG_HINTS:
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
raise ConfigurationError(msg)
for filename in get_env_files(service_dict):
if not os.path.exists(filename):
raise ConfigurationError("Couldn't find env file for service %s: %s" % (service_dict['name'], filename))
if 'environment' in service_dict or 'env_file' in service_dict:
service_dict['environment'] = build_environment(service_dict)
return service_dict
def parse_links(links):
return dict(parse_link(l) for l in links)
def parse_link(link):
if ':' in link:
source, alias = link.split(':', 1)
return (alias, source)
else:
return (link, link)
def get_env_files(options):
env_files = options.get('env_file', [])
if not isinstance(env_files, list):
env_files = [env_files]
return env_files
def build_environment(options):
env = {}
for f in get_env_files(options):
env.update(env_vars_from_file(f))
env.update(parse_environment(options.get('environment')))
return dict(resolve_env(k, v) for k, v in six.iteritems(env))
def parse_environment(environment):
if not environment:
return {}
if isinstance(environment, list):
return dict(split_env(e) for e in environment)
if isinstance(environment, dict):
return environment
raise ConfigurationError(
"environment \"%s\" must be a list or mapping," %
environment
)
def split_env(env):
if '=' in env:
return env.split('=', 1)
else:
return env, None
def resolve_env(key, val):
if val is not None:
return key, val
elif key in os.environ:
return key, os.environ[key]
else:
return key, ''
def env_vars_from_file(filename):
"""
Read in a line delimited file of environment variables.
"""
env = {}
for line in open(filename, 'r'):
line = line.strip()
if line and not line.startswith('#'):
k, v = split_env(line)
env[k] = v
return env
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg

View file

@ -3,6 +3,7 @@ from __future__ import absolute_import
import logging
from functools import reduce
from .config import ConfigurationError
from .service import Service
from .container import Container
from docker.errors import APIError
@ -85,16 +86,6 @@ class Project(object):
volumes_from=volumes_from, **service_dict))
return project
@classmethod
def from_config(cls, name, config, client):
dicts = []
for service_name, service in list(config.items()):
if not isinstance(service, dict):
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name)
service['name'] = service_name
dicts.append(service)
return cls.from_dicts(name, dicts, client)
def get_service(self, name):
"""
Retrieve a service by name. Raises NoSuchService
@ -277,13 +268,5 @@ class NoSuchService(Exception):
return self.msg
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class DependencyError(ConfigurationError):
pass

View file

@ -8,51 +8,14 @@ from operator import attrgetter
import sys
from docker.errors import APIError
import six
from .config import DOCKER_CONFIG_KEYS
from .container import Container, get_container_name
from .progress_stream import stream_output, StreamOutputError
log = logging.getLogger(__name__)
DOCKER_CONFIG_KEYS = [
'cap_add',
'cap_drop',
'cpu_shares',
'command',
'detach',
'dns',
'dns_search',
'domainname',
'entrypoint',
'env_file',
'environment',
'hostname',
'image',
'mem_limit',
'net',
'ports',
'privileged',
'restart',
'stdin_open',
'tty',
'user',
'volumes',
'volumes_from',
'working_dir',
]
DOCKER_CONFIG_HINTS = {
'cpu_share' : 'cpu_shares',
'link' : 'links',
'port' : 'ports',
'privilege' : 'privileged',
'priviliged': 'privileged',
'privilige' : 'privileged',
'volume' : 'volumes',
'workdir' : 'working_dir',
}
DOCKER_START_KEYS = [
'cap_add',
'cap_drop',
@ -96,20 +59,6 @@ class Service(object):
if 'image' in options and 'build' in options:
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
for filename in get_env_files(options):
if not os.path.exists(filename):
raise ConfigError("Couldn't find env file for service %s: %s" % (name, filename))
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose',
'external_links']
for k in options:
if k not in supported_options:
msg = "Unsupported config option for %s service: '%s'" % (name, k)
if k in DOCKER_CONFIG_HINTS:
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
raise ConfigError(msg)
self.name = name
self.client = client
self.project = project
@ -478,8 +427,6 @@ class Service(object):
(parse_volume_spec(v).internal, {})
for v in container_options['volumes'])
container_options['environment'] = build_environment(container_options)
if self.can_be_built():
container_options['image'] = self.full_name
else:
@ -648,63 +595,3 @@ def split_port(port):
external_ip, external_port, internal_port = parts
return internal_port, (external_ip, external_port or None)
def get_env_files(options):
env_files = options.get('env_file', [])
if not isinstance(env_files, list):
env_files = [env_files]
return env_files
def build_environment(options):
env = {}
for f in get_env_files(options):
env.update(env_vars_from_file(f))
env.update(parse_environment(options.get('environment')))
return dict(resolve_env(k, v) for k, v in six.iteritems(env))
def parse_environment(environment):
if not environment:
return {}
if isinstance(environment, list):
return dict(split_env(e) for e in environment)
if isinstance(environment, dict):
return environment
raise ConfigError("environment \"%s\" must be a list or mapping," %
environment)
def split_env(env):
if '=' in env:
return env.split('=', 1)
else:
return env, None
def resolve_env(key, val):
if val is not None:
return key, val
elif key in os.environ:
return key, os.environ[key]
else:
return key, ''
def env_vars_from_file(filename):
"""
Read in a line delimited file of environment variables.
"""
env = {}
for line in open(filename, 'r'):
line = line.strip()
if line and not line.startswith('#'):
k, v = split_env(line)
env[k] = v
return env