def test_ambiguous_project_name(self): config_files = [ ConfigFile(None, {'project_name': 'myproject'}), ConfigFile(None, {'project_name': 'another_name'}) ] with pytest.raises(ConfigurationError) as excinfo: get_config_project_name(config_files) assert 'project_name has multiple values: another_name, myproject' in str(excinfo) \ or 'project_name has multiple values: myproject, another_name' in str(excinfo)
def store_config_file(ctx, compose_file, considered_files): filepath = Path(compose_file.filename) fileparent = filepath.parent compose_file_version = compose_file.version if not isinstance(compose_file_version, ComposeVersion): # up to Docker-Compose 1.14 compose_file_version = ComposeVersion(compose_file_version) put_config_file(ctx, filepath, considered_files) if compose_file_version < ComposeVersion('2'): services = compose_file.config elif compose_file_version < ComposeVersion('4'): services = compose_file.config['services'] else: log.error('Unsupported config version: %s' % compose_file.version) raise SystemExit(1) for service in services.values(): env_files = service.get('env_file', ()) if isinstance(env_files, str): env_files = (env_files, ) for env_file in env_files: env_file_path = fileparent / env_file put_config_file(ctx, env_file_path, considered_files) extends_file = service.get('extends', {}).get('file') if extends_file is None: continue extends_file_path = fileparent / extends_file extends_file = ConfigFile.from_filename(str(extends_file_path)) store_config_file(ctx, extends_file, considered_files)
def build_config_details(contents, working_dir='working_dir', filename='filename.yml'): return ConfigDetails( working_dir, [ConfigFile(filename, contents)], )
def store_config_file(ctx, compose_file, considered_files): filepath = Path(compose_file.filename) fileparent = filepath.parent put_config_file(ctx, filepath, considered_files) if compose_file.version == '1': services = compose_file.config elif compose_file.version.startswith('2.'): services = compose_file.config['services'] else: log.error('Unsupported config version: %s' % compose_file.version) raise SystemExit(1) for service in services.values(): env_files = service.get('env_file', ()) if isinstance(env_files, str): env_files = (env_files,) for env_file in env_files: env_file_path = fileparent / env_file put_config_file(ctx, env_file_path, considered_files) extends_file = service.get('extends', {}).get('file') if extends_file is None: continue extends_file_path = fileparent / extends_file extends_file = ConfigFile.from_filename(str(extends_file_path)) store_config_file(ctx, extends_file, considered_files)
def test_compose(self): from compose.config.config import ConfigFile, ConfigDetails from compose.config.config import load as load_config from compose.project import Project composefile = """ version: '2' services: first: image: alpine command: sleep 3600 second: image: alpine command: sleep 3600 pygen: image: pygen-build command: > --template '# {% for c in containers %} Name={{ c.name }} {% endfor %} 1st={{ containers.matching("first")|length }} 2nd={{ containers.matching("second")|length }}' --one-shot volumes: - /var/run/docker.sock:/var/run/docker.sock:ro depends_on: - first - second """ with open('/tmp/pygen-composefile.yml', 'w') as target: target.write(composefile) config = ConfigFile.from_filename('/tmp/pygen-composefile.yml') details = ConfigDetails('/tmp', [config]) project = Project.from_config('cmpse', load_config(details), self.remote_client.api) with self.suppress_stderr(): project.up(detached=True, scale_override={'second': 2}) pygen_service = project.get_service('pygen') pygen_container = next(iter(pygen_service.containers())) pygen_container.wait() output = ''.join(pygen_container.logs(stdout=True, stderr=False)) self.assertIn('Name=cmpse_first_1', output) self.assertIn('Name=cmpse_second_1', output) self.assertIn('Name=cmpse_second_2', output) self.assertIn('Name=cmpse_pygen_1', output) self.assertIn('1st=1', output) self.assertIn('2nd=2', output)
def __init__(self, name, working_dir, config_file): config_file_path = os.path.join(working_dir, config_file) cfg_file = ConfigFile.from_filename(config_file_path) c = ConfigDetails( working_dir, [cfg_file], ) self.cd = load(c) self.name = name
def format_dot(yml_path): """ :param yml_path: str, path of the docker-compose yml to draw. :return: str, formatted dot template describing the graph. """ from compose.config.config import ConfigFile config_file = ConfigFile.from_filename(yml_path) return template.format( nodes=format_nodes(config_file), links=format_links(config_file) )
def __init__(self, project_name, directory, composefile="docker-compose.yml", output="{{ result }}", **invocations): config = ConfigFile.from_filename("%s/%s" % (directory, composefile)) details = ConfigDetails(directory, [config]) self.project = Project.from_config(project_name, load_config(details), self.client.api) super(DockerComposeAction, self).__init__(output, **invocations)
def update_image(filename, new_image, service_name='web'): """ Update service image name to new_image. """ path = os.path.dirname(filename) conf_file = ConfigFile.from_filename(filename) conf = load(ConfigDetails(path, [conf_file], None)) # find service for i in range(len(conf.services)): service = conf.services[i] if service['name'] == service_name: conf.services[i]['image'] = new_image out = open(filename, 'w') out.write(serialize_config(conf)) out.close() return filename
def build(filename, env_dict=None, output_path=None): """ Build docker-compose.yml file from services & env. """ path = os.path.dirname(filename) conf_file = ConfigFile.from_filename(filename) env = Environment(env_dict) if env_dict else None conf = load(ConfigDetails(path, [conf_file], env)) output_path = output_path if output_path else path + '/docker-compose.yml' # try to make directory if os.path.dirname(output_path): os.makedirs(os.path.dirname(output_path)) out = open(output_path, 'w') out.write(serialize_config(conf)) out.close() return output_path
def start_compose_project(self, name, directory, composefile_name, composefile_contents=None): if composefile_contents: with open(relative_path('%s/%s' % (directory, composefile_name)), 'w') as composefile: composefile.write(composefile_contents) config = ConfigFile.from_filename( relative_path('%s/%s' % (directory, composefile_name))) details = ConfigDetails(relative_path(directory), [config]) project = Project.from_config(name, load_config(details), self.docker_client.api) self.started_compose_projects.append(project) project.up(detached=True) if composefile_contents: os.remove(relative_path('%s/%s' % (directory, composefile_name))) return project
def test_restart_compose_service(self): from compose.config.config import ConfigFile, ConfigDetails from compose.config.config import load as load_config from compose.project import Project composefile = """ version: '2' services: app: image: alpine command: sh -c 'date +%s ; sleep 3600' pygen: image: pygen-build command: > --template '#ok' --restart app --one-shot volumes: - /var/run/docker.sock:/var/run/docker.sock:ro depends_on: - app """ with open('/tmp/pygen-composefile.yml', 'w') as target: target.write(composefile) config = ConfigFile.from_filename('/tmp/pygen-composefile.yml') details = ConfigDetails('/tmp', [config]) project = Project.from_config('cmpse', load_config(details), self.remote_client.api) with self.suppress_stderr(): project.up(detached=True, service_names=['app'], scale_override={'app': 2}) app = project.get_service('app') for _ in range(60): if len(app.containers()) < 2 or not all( c.is_running for c in app.containers()): self.wait(0.5) initial_logs = list(''.join( c.logs(stdout=True) for c in app.containers()).splitlines()) project.up(detached=True, scale_override={'app': 2}) pygen_service = project.get_service('pygen') pygen_container = next(iter(pygen_service.containers())) pygen_container.wait() for _ in range(60): if len(app.containers()) < 2 or not all( c.is_running for c in app.containers()): self.wait(0.5) newer_logs = list(''.join( c.logs(stdout=True) for c in app.containers()).splitlines()) self.assertNotEqual(tuple(sorted(newer_logs)), tuple(sorted(initial_logs))) self.assertEqual(len(newer_logs), 4)
OUTPUT_DIR = 'release' WORKDIR = os.path.abspath(os.curdir) REPLACE_MOUNTS = {} if not os.path.exists(os.path.join(WORKDIR, '.env')): copyfile( os.path.join(WORKDIR, './.env.example'), os.path.join(WORKDIR, './.env'), ) configs = [] for file in CONFIGS: print(f'Reading file: {file}') with open(file, 'r') as f: configs.append(ConfigFile(None, yaml.safe_load(f.read()))) print('Building config') env = config.environment.Environment() details = ConfigDetails(WORKDIR, configs, env) cfg = config.load(details, False) def relativize(path: str) -> str: result = f'./{os.path.relpath(path, WORKDIR)}' if path and path.startswith( WORKDIR) else path if result in REPLACE_MOUNTS: result = REPLACE_MOUNTS[result] return result
# # Possible values - LR, RL, BT, TB... for left->right, bottom->top, etc. DIRECTION="TB" import sys import yaml import pygraphviz as pgv from collections import defaultdict # seemingly undocumented magic that might prove useful from compose.config.config import ConfigFile # arg sanity check try: dcompose = ConfigFile.from_filename(sys.argv[1]) except IndexError as e: print "Usage: file.yml" sys.exit(1) except Exception as e: print "Error: %s" % e sys.exit(2) filename = sys.argv[1] # assume it's a foo.bar kinda file filebase = filename.split('.')[0] # this is where we draw the relationships compose = defaultdict(lambda : defaultdict(list)) # various things to look for inside compose file
def test_single_config_file(self): config_files = [ConfigFile(None, {'project_name': 'myproject'})] assert get_config_project_name(config_files) == 'myproject'
def test_undefined_project_name(self): config_files = [ConfigFile(None, {})] assert get_config_project_name(config_files) is None
def test_build_native_builder_called(self, cli_build): options = { '--build-arg': ['MYVAR', 'ARG=123'], '--no-cache': True, '--pull': True, '--force-rm': False, '--memory': True, '--compress': False, '--parallel': False, '--quiet': True, '--progress': 'progress', 'SERVICE': ['service'], 'COMMAND': 'build', } env = Environment({ 'MYVAR': 'MYVALUE', }) if cli_build: env['COMPOSE_DOCKER_CLI_BUILD'] = '1' env['COMPOSE_DOCKER_CLI_BUILD_EXTRA_ARGS'] = '--extra0 --extra1=1' iidfile = [None] def mock_mktemp(): iidfile[0] = tempfile.mktemp() with open(iidfile[0], 'w') as f: f.write(':12345') return iidfile[0] with mock.patch('compose.cli.main.TopLevelCommand.toplevel_environment', new=env), \ mock.patch('compose.cli.main.Environment.from_env_file', return_value=env), \ mock.patch('compose.service.subprocess.Popen') as mock_subprocess_popen, \ mock.patch('compose.service.tempfile', new=mock.Mock(mktemp=mock_mktemp)), \ mock.patch('compose.cli.command.get_client') as mock_get_client, \ mock.patch('compose.cli.command.config.find') as mock_config_find, \ mock.patch('compose.cli.command.config.load') as mock_config_load: mock_config_find.return_value = ConfigDetails( working_dir='working_dir', config_files=[ConfigFile(filename='config_file', config={})], environment=env, ) mock_config_load.return_value = Config( version=COMPOSEFILE_V3_4, services=[{ 'name': 'service', 'build': { 'context': '.', }, }], volumes={}, networks={}, secrets={}, configs={}, ) mock_get_client.return_value.api_version = '1.35' mock_build = mock_get_client.return_value.build mock_build.return_value = \ mock_subprocess_popen.return_value.__enter__.return_value.stdout = \ io.StringIO('{"stream": "Successfully built 12345"}') project = [None] def handler(command, options): project[0] = command.project command.build(options) perform_command(options, handler=handler, command_options=options) if not cli_build: assert mock_build.called assert mock_build.call_args[1]['buildargs'] == { 'MYVAR': 'MYVALUE', 'ARG': '123' } assert mock_build.call_args[1]['pull'] assert mock_build.call_args[1]['nocache'] assert not mock_build.call_args[1]['forcerm'] assert not mock_build.call_args[1]['gzip'] assert not project[0].native_build_enabled else: assert mock_subprocess_popen.call_args[0][0] == [ 'docker', 'build', '--build-arg', 'MYVAR=MYVALUE', '--build-arg', 'ARG=123', '--memory', 'True', '--no-cache', '--progress', 'progress', '--pull', '--tag', 'working_dir_service', '--iidfile', iidfile[0], '--extra0', '--extra1=1', '.', ] assert project[0].native_build_enabled
def test_build_native_args_propagated(self, cli_build): options = { '--build-arg': ['MYVAR', 'ARG=123'], '--no-cache': True, '--pull': True, '--force-rm': True, '--memory': True, '--compress': True, '--parallel': True, '--quiet': True, '--progress': 'progress', 'SERVICE': ['service'], 'COMMAND': 'build', } env = Environment({ 'MYVAR': 'MYVALUE', }) if cli_build: env['COMPOSE_DOCKER_CLI_BUILD'] = '1' with mock.patch('compose.cli.main.TopLevelCommand.toplevel_environment', new=env), \ mock.patch('compose.cli.main.Environment.from_env_file', return_value=env), \ mock.patch('compose.project.Project.build') as mock_build, \ mock.patch('compose.cli.command.config.find') as mock_config_find, \ mock.patch('compose.cli.command.config.load') as mock_config_load: mock_config_find.return_value = ConfigDetails( working_dir='working_dir', config_files=[ConfigFile(filename='config_file', config={})], environment=env, ) mock_config_load.return_value = Config( version=COMPOSEFILE_V3_4, services=[], volumes={}, networks={}, secrets={}, configs={}, ) project = [None] def handler(command, options): project[0] = command.project command.build(options) perform_command(options, handler=handler, command_options=options) assert mock_build.call_args == mock.call( service_names=['service'], no_cache=True, pull=True, force_rm=True, memory=True, rm=True, build_args={ 'MYVAR': 'MYVALUE', 'ARG': '123' }, gzip=True, parallel_build=True, silent=True, progress='progress', ) assert project[0].native_build_enabled == bool(cli_build)
def test_duplicated_project_name(self): config_files = [ ConfigFile(None, {'project_name': 'myproject'}), ConfigFile(None, {'project_name': 'myproject'}) ] assert get_config_project_name(config_files) == 'myproject'