Add docker-compose config subcommand.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2015-10-10 14:41:17 -04:00
parent ecf2dd11f9
commit 437f3f8adb
4 changed files with 82 additions and 10 deletions

View File

@ -46,7 +46,7 @@ def friendly_error_message():
def project_from_options(base_dir, options): def project_from_options(base_dir, options):
return get_project( return get_project(
base_dir, base_dir,
get_config_path(options.get('--file')), get_config_path_from_options(options),
project_name=options.get('--project-name'), project_name=options.get('--project-name'),
verbose=options.get('--verbose'), verbose=options.get('--verbose'),
use_networking=options.get('--x-networking'), use_networking=options.get('--x-networking'),
@ -54,7 +54,8 @@ def project_from_options(base_dir, options):
) )
def get_config_path(file_option): def get_config_path_from_options(options):
file_option = options.get('--file')
if file_option: if file_option:
return file_option return file_option

View File

@ -8,10 +8,12 @@ import sys
from inspect import getdoc from inspect import getdoc
from operator import attrgetter from operator import attrgetter
import yaml
from docker.errors import APIError from docker.errors import APIError
from requests.exceptions import ReadTimeout from requests.exceptions import ReadTimeout
from .. import __version__ from .. import __version__
from ..config import config
from ..config import ConfigurationError from ..config import ConfigurationError
from ..config import parse_environment from ..config import parse_environment
from ..const import DEFAULT_TIMEOUT from ..const import DEFAULT_TIMEOUT
@ -23,6 +25,7 @@ from ..service import BuildError
from ..service import ConvergenceStrategy from ..service import ConvergenceStrategy
from ..service import NeedsBuildError from ..service import NeedsBuildError
from .command import friendly_error_message from .command import friendly_error_message
from .command import get_config_path_from_options
from .command import project_from_options from .command import project_from_options
from .docopt_command import DocoptCommand from .docopt_command import DocoptCommand
from .docopt_command import NoSuchCommand from .docopt_command import NoSuchCommand
@ -126,6 +129,7 @@ class TopLevelCommand(DocoptCommand):
Commands: Commands:
build Build or rebuild services build Build or rebuild services
config Validate and view the compose file
help Get help on a command help Get help on a command
kill Kill containers kill Kill containers
logs View output from containers logs View output from containers
@ -158,6 +162,10 @@ class TopLevelCommand(DocoptCommand):
handler(None, command_options) handler(None, command_options)
return return
if options['COMMAND'] == 'config':
handler(options, command_options)
return
project = project_from_options(self.base_dir, options) project = project_from_options(self.base_dir, options)
with friendly_error_message(): with friendly_error_message():
handler(project, command_options) handler(project, command_options)
@ -183,6 +191,36 @@ class TopLevelCommand(DocoptCommand):
pull=bool(options.get('--pull', False)), pull=bool(options.get('--pull', False)),
force_rm=bool(options.get('--force-rm', False))) force_rm=bool(options.get('--force-rm', False)))
def config(self, config_options, options):
"""
Validate and view the compose file.
Usage: config [options]
Options:
-q, --quiet Only validate the configuration, don't print
anything.
--services Print the service names, one per line.
"""
config_path = get_config_path_from_options(config_options)
compose_config = config.load(config.find(self.base_dir, config_path))
if options['--quiet']:
return
if options['--services']:
print('\n'.join(service['name'] for service in compose_config))
return
compose_config = dict(
(service.pop('name'), service) for service in compose_config)
print(yaml.dump(
compose_config,
default_flow_style=False,
indent=2,
width=80))
def help(self, project, options): def help(self, project, options):
""" """
Get help on a command. Get help on a command.

View File

@ -7,6 +7,7 @@ import subprocess
import time import time
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from textwrap import dedent
from docker import errors from docker import errors
@ -90,10 +91,11 @@ class CLITestCase(DockerClientTestCase):
self.base_dir = 'tests/fixtures/simple-composefile' self.base_dir = 'tests/fixtures/simple-composefile'
def tearDown(self): def tearDown(self):
self.project.kill() if self.base_dir:
self.project.remove_stopped() self.project.kill()
for container in self.project.containers(stopped=True, one_off=True): self.project.remove_stopped()
container.remove(force=True) for container in self.project.containers(stopped=True, one_off=True):
container.remove(force=True)
super(CLITestCase, self).tearDown() super(CLITestCase, self).tearDown()
@property @property
@ -109,13 +111,39 @@ class CLITestCase(DockerClientTestCase):
return wait_on_process(proc, returncode=returncode) return wait_on_process(proc, returncode=returncode)
def test_help(self): def test_help(self):
old_base_dir = self.base_dir
self.base_dir = 'tests/fixtures/no-composefile' self.base_dir = 'tests/fixtures/no-composefile'
result = self.dispatch(['help', 'up'], returncode=1) result = self.dispatch(['help', 'up'], returncode=1)
assert 'Usage: up [options] [SERVICE...]' in result.stderr assert 'Usage: up [options] [SERVICE...]' in result.stderr
# self.project.kill() fails during teardown # Prevent tearDown from trying to create a project
# unless there is a composefile. self.base_dir = None
self.base_dir = old_base_dir
def test_config_list_services(self):
result = self.dispatch(['config', '--services'])
assert set(result.stdout.rstrip().split('\n')) == {'simple', 'another'}
def test_config_quiet_with_error(self):
self.base_dir = None
result = self.dispatch([
'-f', 'tests/fixtures/invalid-composefile/invalid.yml',
'config', '-q'
], returncode=1)
assert "'notaservice' doesn't have any configuration" in result.stderr
def test_config_quiet(self):
assert self.dispatch(['config', '-q']).stdout == ''
def test_config_default(self):
result = self.dispatch(['config'])
assert dedent("""
simple:
command: top
image: busybox:latest
""").lstrip() in result.stdout
assert dedent("""
another:
command: top
image: busybox:latest
""").lstrip() in result.stdout
def test_ps(self): def test_ps(self):
self.project.get_service('simple').create_container() self.project.get_service('simple').create_container()

View File

@ -0,0 +1,5 @@
notaservice: oops
web:
image: 'alpine:edge'