def apply(config_id, config, managed_by, labels=None, docker_cmd=None): """Execute supplied container configuration. :param str config_id: Unique config ID, should not be re-used until any running containers with that config ID have been deleted. :param dict config: Configuration data describing container actions to apply. :param str managed_by: Name of the tool managing the containers. Only containers labelled with this will be modified. :param dict labels: Optional keys/values of labels to apply to containers created with this invocation. :param str docker_cmd: Optional override to the docker command to run. :returns (list, list, int) lists of stdout and stderr for each execution, and a single return code representing the overall success of the apply. :rtype: tuple """ r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd) builder = compose1.ComposeV1Builder(config_id=config_id, config=config, runner=r, labels=labels) return builder.apply()
def test_container_run_args_lists(self): config = { 'one': { 'image': 'centos:7', 'detach': False, 'command': 'ls -l /foo', 'remove': True, 'tty': True, 'interactive': True, 'environment': ['FOO=BAR', 'BAR=BAZ'], 'env_file': ['/tmp/foo.env', '/tmp/bar.env'], 'ulimit': ['nofile=1024', 'nproc=1024'], 'volumes': ['/foo:/foo:rw', '/bar:/bar:ro'], 'volumes_from': ['two', 'three'], 'group_add': ['docker', 'zuul'], 'cap_add': ['SYS_ADMIN', 'SETUID'], 'cap_drop': ['NET_RAW'] } } builder = compose1.ComposeV1Builder('foo', config, None) cmd = ['docker', 'run', '--name', 'one'] builder.container_run_args(cmd, 'one') self.assertEqual([ 'docker', 'run', '--name', 'one', '--env-file=/tmp/foo.env', '--env-file=/tmp/bar.env', '--env=FOO=BAR', '--env=BAR=BAZ', '--rm', '--interactive', '--tty', '--ulimit=nofile=1024', '--ulimit=nproc=1024', '--group-add=docker', '--group-add=zuul', '--volume=/foo:/foo:rw', '--volume=/bar:/bar:ro', '--volumes-from=two', '--volumes-from=three', '--cap-add=SYS_ADMIN', '--cap-add=SETUID', '--cap-drop=NET_RAW', 'centos:7', 'ls', '-l', '/foo' ], cmd)
def test_label_arguments(self, runner): r = runner.return_value r.managed_by = 'tester' builder = compose1.ComposeV1Builder('foo', {}, r) cmd = [] builder.label_arguments(cmd, 'one') self.assertEqual([ '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester' ], cmd) labels = {'foo': 'bar', 'bar': 'baz'} builder = compose1.ComposeV1Builder('foo', {}, r, labels=labels) cmd = [] builder.label_arguments(cmd, 'one') self.assertEqual([ '--label', 'foo=bar', '--label', 'bar=baz', '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester' ], cmd)
def apply(config_id, config, managed_by, labels=None, cont_cmd='podman', default_runtime=None, log_level=None, log_file=None, cont_log_path=None, healthcheck_disabled=False): """Execute supplied container configuration. :param str config_id: Unique config ID, should not be re-used until any running containers with that config ID have been deleted. :param dict config: Configuration data describing container actions to apply. :param str managed_by: Name of the tool managing the containers. Only containers labelled with this will be modified. :param dict labels: Optional keys/values of labels to apply to containers created with this invocation. :param str cont_cmd: Optional override to the container command to run. :param str default_runtime: (deprecated) does nothing. :param int log_level: optional log level for loggers :param str log_file: optional log file for messages :param str cont_log_path: optional log path for containers. Works only for podman engine. Must be an absolute path. :param bool healthcheck_disabled: optional boolean to disable container healthcheck. :returns (list, list, int) lists of stdout and stderr for each execution, and a single return code representing the overall success of the apply. :rtype: tuple """ log = common.configure_logging(__name__, log_level, log_file) if default_runtime: log.warning("DEPRECATION: 'default_runtime' does nothing, " "use 'cont_cmd' instead") if cont_cmd == 'podman': r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log) builder = podman.PodmanBuilder( config_id=config_id, config=config, runner=r, labels=labels, log=log, cont_log_path=cont_log_path, healthcheck_disabled=healthcheck_disabled ) else: r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log) builder = compose1.ComposeV1Builder( config_id=config_id, config=config, runner=r, labels=labels, log=log ) return builder.apply()
def test_label_arguments(self, runner): r = runner.return_value r.managed_by = 'tester' builder = compose1.ComposeV1Builder('foo', {}, r) cmd = [] builder.label_arguments(cmd, 'one') self.assertEqual([ '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester', '--label', 'config_data=null' ], cmd) labels = collections.OrderedDict() labels['foo'] = 'bar' labels['bar'] = 'baz' builder = compose1.ComposeV1Builder('foo', {}, r, labels=labels) cmd = [] builder.label_arguments(cmd, 'one') self.assertEqual([ '--label', 'foo=bar', '--label', 'bar=baz', '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester', '--label', 'config_data=null' ], cmd)
def test_cont_run_args(self): config = { 'one': { 'image': 'centos:7', 'privileged': True, 'user': '******', 'net': 'host', 'ipc': 'host', 'pid': 'container:bar', 'uts': 'host', 'restart': 'always', 'healthcheck': { 'test': '/bin/true', 'interval': '30s', 'timeout': '10s', 'retries': 3 }, 'env_file': '/tmp/foo.env', 'log_tag': '{{.ImageName}}/{{.Name}}/{{.ID}}', 'cpu_shares': 600, 'mem_limit': '1G', 'memswap_limit': '1G', 'mem_swappiness': '60', 'security_opt': 'label:disable', 'cap_add': ['SYS_ADMIN', 'SETUID'], 'cap_drop': ['NET_RAW'] } } builder = compose1.ComposeV1Builder('foo', config, None) cmd = ['docker', 'run', '--name', 'one'] builder.container_run_args(cmd, 'one') self.assertEqual( ['docker', 'run', '--name', 'one', '--detach=true', '--env-file=/tmp/foo.env', '--net=host', '--ipc=host', '--pid=container:bar', '--uts=host', '--health-cmd=/bin/true', '--health-interval=30s', '--health-timeout=10s', '--health-retries=3', '--privileged=true', '--restart=always', '--user=bar', '--log-opt=tag={{.ImageName}}/{{.Name}}/{{.ID}}', '--cpu-shares=600', '--memory=1G', '--memory-swap=1G', '--memory-swappiness=60', '--security-opt=label:disable', '--cap-add=SYS_ADMIN', '--cap-add=SETUID', '--cap-drop=NET_RAW', 'centos:7'], cmd )
def test_durations(self): config = { 'a': { 'stop_grace_period': 123 }, 'b': { 'stop_grace_period': 123.5 }, 'c': { 'stop_grace_period': '123.3' }, 'd': { 'stop_grace_period': '2.5s' }, 'e': { 'stop_grace_period': '10s' }, 'f': { 'stop_grace_period': '1m30s' }, 'g': { 'stop_grace_period': '2h32m' }, 'h': { 'stop_grace_period': '5h34m56s' }, 'i': { 'stop_grace_period': '1h1m1s1ms1us' }, } builder = compose1.ComposeV1Builder('foo', config, None) result = { 'a': '--stop-timeout=123', 'b': '--stop-timeout=123.5', 'c': '--stop-timeout=123.3', 'd': '--stop-timeout=2.5', 'e': '--stop-timeout=10.0', 'f': '--stop-timeout=90.0', 'g': '--stop-timeout=9120.0', 'h': '--stop-timeout=20096.0', 'i': '--stop-timeout=3661.001001', } for container, arg in result.items(): cmd = [] builder.container_run_args(cmd, container) self.assertIn(arg, cmd)
def test_docker_exec_args(self, runner): r = runner.return_value r.discover_container_name.return_value = 'one-12345678' config = { 'one': { 'command': 'ls -l /foo', 'privileged': True, 'user': '******' } } self.builder = compose1.ComposeV1Builder('foo', config, runner.return_value) cmd = ['docker', 'exec'] self.builder.docker_exec_args(cmd, 'one') self.assertEqual([ 'docker', 'exec', '--privileged=true', '--user=bar', 'one-12345678', '-l', '/foo' ], cmd)
def test_docker_run_args(self): config = { 'one': { 'image': 'centos:7', 'privileged': True, 'user': '******', 'net': 'host', 'pid': 'container:bar', 'restart': 'always', 'env_file': '/tmp/foo.env', } } builder = compose1.ComposeV1Builder('foo', config, None) cmd = ['docker', 'run', '--name', 'one'] builder.docker_run_args(cmd, 'one') self.assertEqual([ 'docker', 'run', '--name', 'one', '--detach=true', '--env-file=/tmp/foo.env', '--net=host', '--pid=container:bar', '--privileged=true', '--restart=always', '--user=bar', 'centos:7' ], cmd)
def test_docker_run_args_lists(self): config = { 'one': { 'image': 'centos:7', 'detach': False, 'command': 'ls -l /foo', 'environment': ['FOO=BAR', 'BAR=BAZ'], 'env_file': ['/tmp/foo.env', '/tmp/bar.env'], 'volumes': ['/foo:/foo:rw', '/bar:/bar:ro'], 'volumes_from': ['two', 'three'] } } builder = compose1.ComposeV1Builder('foo', config, None) cmd = ['docker', 'run', '--name', 'one'] builder.docker_run_args(cmd, 'one') self.assertEqual([ 'docker', 'run', '--name', 'one', '--env-file=/tmp/foo.env', '--env-file=/tmp/bar.env', '--env=FOO=BAR', '--env=BAR=BAZ', '--volume=/foo:/foo:rw', '--volume=/bar:/bar:ro', '--volumes-from=two', '--volumes-from=three', 'centos:7', 'ls', '-l', '/foo' ], cmd)
def test_apply(self, runner): config = { 'one': { 'start_order': 0, 'image': 'centos:7', }, 'two': { 'start_order': 1, 'image': 'centos:7', }, 'three': { 'start_order': 2, 'image': 'centos:7', }, 'four': { 'start_order': 10, 'image': 'centos:7', }, 'four_ls': { 'action': 'exec', 'start_order': 20, 'command': ['four', 'ls', '-l', '/'] } } r = runner.return_value r.managed_by = 'tester' r.discover_container_name = lambda n, c: '%s-12345678' % n r.unique_container_name = lambda n: '%s-12345678' % n r.docker_cmd = 'docker' r.execute.return_value = ('Done!', '', 0) builder = compose1.ComposeV1Builder('foo', config, r) stdout, stderr, deploy_status_code = builder.apply() self.assertEqual(0, deploy_status_code) self.assertEqual(['Done!', 'Done!', 'Done!', 'Done!', 'Done!'], stdout) self.assertEqual([], stderr) r.execute.assert_has_calls([ mock.call([ 'docker', 'run', '--name', 'one-12345678', '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester', '--detach=true', 'centos:7' ]), mock.call([ 'docker', 'run', '--name', 'two-12345678', '--label', 'config_id=foo', '--label', 'container_name=two', '--label', 'managed_by=tester', '--detach=true', 'centos:7' ]), mock.call([ 'docker', 'run', '--name', 'three-12345678', '--label', 'config_id=foo', '--label', 'container_name=three', '--label', 'managed_by=tester', '--detach=true', 'centos:7' ]), mock.call([ 'docker', 'run', '--name', 'four-12345678', '--label', 'config_id=foo', '--label', 'container_name=four', '--label', 'managed_by=tester', '--detach=true', 'centos:7' ]), mock.call(['docker', 'exec', 'four-12345678', 'ls', '-l', '/']), ])
def debug(config_id, container_name, action, config, managed_by, labels=None, cont_cmd='podman', default_runtime=None, log_level=None, log_file=None): """Execute supplied container configuration. :param str config_id: Unique config ID, should not be re-used until any running containers with that config ID have been deleted. :param str container_name: Name of the container in the config you wish to manipulate. :param str action: Action to take. :param dict config: Configuration data describing container actions to apply. :param str managed_by: Name of the tool managing the containers. Only containers labeled with this will be modified. :param dict labels: Optional keys/values of labels to apply to containers created with this invocation. :param str cont_cmd: Optional override to the container command to run. :param str default_runtime: (deprecated) does nothing. :param int log_level: optional log level for loggers :param int log_file: optional log file for messages :returns integer return value from running command or failure for any other reason. :rtype: int """ log = common.configure_logging(__name__, log_level, log_file) if default_runtime: log.warning("DEPRECATION: 'default_runtime' does nothing, " "use 'cont_cmd' instead") if cont_cmd == 'podman': r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log) builder = podman.PodmanBuilder( config_id=config_id, config=config, runner=r, labels=labels, log=log ) else: r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log) builder = compose1.ComposeV1Builder( config_id=config_id, config=config, runner=r, labels=labels, log=log ) if action == 'print-cmd': cmd = [ r.cont_cmd, 'run', '--name', r.unique_container_name(container_name) ] builder.container_run_args(cmd, container_name) print(' '.join(cmd)) elif action == 'run': cmd = [ r.cont_cmd, 'run', '--name', r.unique_container_name(container_name) ] builder.container_run_args(cmd, container_name) return r.execute_interactive(cmd, log) elif action == 'dump-yaml': print(yaml.safe_dump(config, default_flow_style=False)) elif action == 'dump-json': print(json.dumps(config, indent=4)) else: raise ValueError('action should be one of: "dump-json", "dump-yaml"', '"print-cmd", or "run"')
def test_apply_failed_pull(self): orig_call = tenacity.wait.wait_random_exponential.__call__ orig_argspec = inspect.getargspec(orig_call) config = { 'one': { 'start_order': 0, 'image': 'centos:7', }, 'two': { 'start_order': 1, 'image': 'centos:7', }, 'three': { 'start_order': 2, 'image': 'centos:6', }, 'four': { 'start_order': 10, 'image': 'centos:7', }, 'four_ls': { 'action': 'exec', 'start_order': 20, 'command': ['four', 'ls', '-l', '/'] } } r = runner.DockerRunner(managed_by='tester', cont_cmd='docker') exe = mock.Mock() exe.side_effect = [ ('exists', '', 0), # inspect for image centos:6 ('', '', 1), # inspect for missing image centos:7 ('Pulling centos:7', 'ouch', 1), # pull centos:7 failure ('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 2 ('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 3 ('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 4 ] r.execute = exe with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f: f.return_value = 0 with mock.patch('inspect.getargspec') as mock_args: mock_args.return_value = orig_argspec builder = compose1.ComposeV1Builder('foo', config, r) stdout, stderr, deploy_status_code = builder.apply() self.assertEqual(1, deploy_status_code) self.assertEqual(['Pulling centos:7'], stdout) self.assertEqual(['ouch'], stderr) exe.assert_has_calls([ # inspect existing image centos:6 mock.call([ 'docker', 'inspect', '--type', 'image', '--format', 'exists', 'centos:6' ], mock.ANY, False), # inspect and pull missing image centos:7 mock.call([ 'docker', 'inspect', '--type', 'image', '--format', 'exists', 'centos:7' ], mock.ANY, False), mock.call(['docker', 'pull', 'centos:7'], mock.ANY), ])
def test_apply(self): orig_call = tenacity.wait.wait_random_exponential.__call__ orig_argspec = inspect.getargspec(orig_call) config = { 'one': { 'start_order': 0, 'image': 'centos:7', }, 'two': { 'start_order': 1, 'image': 'centos:7', }, 'three': { 'start_order': 2, 'image': 'centos:6', }, 'four': { 'start_order': 10, 'image': 'centos:7', }, 'four_ls': { 'action': 'exec', 'start_order': 20, 'command': ['four', 'ls', '-l', '/'] } } r = runner.DockerRunner(managed_by='tester', cont_cmd='docker') exe = mock.Mock() exe.side_effect = [ ('exists', '', 0), # inspect for image centos:6 ('', '', 1), # inspect for missing image centos:7 ('Pulled centos:7', 'ouch', 1), # pull centos:6 fails ('Pulled centos:7', '', 0), # pull centos:6 succeeds ('', '', 0), # ps for delete_missing_and_updated container_names ('', '', 0), # ps for after delete_missing_and_updated renames ('', '', 0), # ps to only create containers which don't exist ('Created one-12345678', '', 0), ('Created two-12345678', '', 0), ('Created three-12345678', '', 0), ('Created four-12345678', '', 0), ('a\nb\nc', '', 0) ] r.discover_container_name = lambda n, c: '%s-12345678' % n r.unique_container_name = lambda n: '%s-12345678' % n r.execute = exe with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f: f.return_value = 0 with mock.patch('inspect.getargspec') as mock_args: mock_args.return_value = orig_argspec builder = compose1.ComposeV1Builder('foo', config, r) stdout, stderr, deploy_status_code = builder.apply() self.assertEqual(0, deploy_status_code) self.assertEqual([ 'Pulled centos:7', 'Created one-12345678', 'Created two-12345678', 'Created three-12345678', 'Created four-12345678', 'a\nb\nc' ], stdout) self.assertEqual([], stderr) exe.assert_has_calls([ # inspect existing image centos:6 mock.call([ 'docker', 'inspect', '--type', 'image', '--format', 'exists', 'centos:6' ], mock.ANY, False), # inspect and pull missing image centos:7 mock.call([ 'docker', 'inspect', '--type', 'image', '--format', 'exists', 'centos:7' ], mock.ANY, False), # first pull attempt fails mock.call(['docker', 'pull', 'centos:7'], mock.ANY), # second pull attempt succeeds mock.call(['docker', 'pull', 'centos:7'], mock.ANY), # ps for delete_missing_and_updated container_names mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--filter', 'label=config_id=foo', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # ps for after delete_missing_and_updated renames mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # ps to only create containers which don't exist mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--filter', 'label=config_id=foo', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # run one mock.call([ 'docker', 'run', '--name', 'one-12345678', '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['one']), '--detach=true', 'centos:7' ], mock.ANY), # run two mock.call([ 'docker', 'run', '--name', 'two-12345678', '--label', 'config_id=foo', '--label', 'container_name=two', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['two']), '--detach=true', 'centos:7' ], mock.ANY), # run three mock.call([ 'docker', 'run', '--name', 'three-12345678', '--label', 'config_id=foo', '--label', 'container_name=three', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['three']), '--detach=true', 'centos:6' ], mock.ANY), # run four mock.call([ 'docker', 'run', '--name', 'four-12345678', '--label', 'config_id=foo', '--label', 'container_name=four', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['four']), '--detach=true', 'centos:7' ], mock.ANY), # execute within four mock.call(['docker', 'exec', 'four-12345678', 'ls', '-l', '/'], mock.ANY), ])
def test_apply_idempotency(self): config = { # not running yet 'one': { 'start_order': 0, 'image': 'centos:7', }, # running, but with a different config 'two': { 'start_order': 1, 'image': 'centos:7', }, # running with the same config 'three': { 'start_order': 2, 'image': 'centos:7', }, # not running yet 'four': { 'start_order': 10, 'image': 'centos:7', }, 'four_ls': { 'action': 'exec', 'start_order': 20, 'command': ['four', 'ls', '-l', '/'] } } r = runner.DockerRunner(managed_by='tester', cont_cmd='docker') exe = mock.Mock() exe.side_effect = [ # inspect for image centos:7 ('exists', '', 0), # ps for delete_missing_and_updated container_names ('''five five six six two-12345678 two three-12345678 three''', '', 0), # rm five ('', '', 0), # rm six ('', '', 0), # inspect two ('{"start_order": 1, "image": "centos:6"}', '', 0), # rm two, changed config data ('', '', 0), # inspect three ('{"start_order": 2, "image": "centos:7"}', '', 0), # ps for after delete_missing_and_updated renames ('', '', 0), # ps to only create containers which don't exist ('three-12345678 three', '', 0), ('Created one-12345678', '', 0), ('Created two-12345678', '', 0), ('Created four-12345678', '', 0), ('a\nb\nc', '', 0) ] r.discover_container_name = lambda n, c: '%s-12345678' % n r.unique_container_name = lambda n: '%s-12345678' % n r.execute = exe builder = compose1.ComposeV1Builder('foo', config, r) stdout, stderr, deploy_status_code = builder.apply() self.assertEqual(0, deploy_status_code) self.assertEqual([ 'Created one-12345678', 'Created two-12345678', 'Created four-12345678', 'a\nb\nc' ], stdout) self.assertEqual([], stderr) exe.assert_has_calls([ # inspect image centos:7 mock.call([ 'docker', 'inspect', '--type', 'image', '--format', 'exists', 'centos:7' ], mock.ANY, False), # ps for delete_missing_and_updated container_names mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--filter', 'label=config_id=foo', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # rm containers not in config mock.call(['docker', 'rm', '-f', 'five'], mock.ANY), mock.call(['docker', 'rm', '-f', 'six'], mock.ANY), # rm two, changed config mock.call([ 'docker', 'inspect', '--type', 'container', '--format', '{{index .Config.Labels "config_data"}}', 'two-12345678' ], mock.ANY, False), mock.call(['docker', 'rm', '-f', 'two-12345678'], mock.ANY), # check three, config hasn't changed mock.call([ 'docker', 'inspect', '--type', 'container', '--format', '{{index .Config.Labels "config_data"}}', 'three-12345678' ], mock.ANY, False), # ps for after delete_missing_and_updated renames mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # ps to only create containers which don't exist mock.call([ 'docker', 'ps', '-a', '--filter', 'label=managed_by=tester', '--filter', 'label=config_id=foo', '--format', '{{.Names}} {{.Label "container_name"}}' ], mock.ANY), # run one mock.call([ 'docker', 'run', '--name', 'one-12345678', '--label', 'config_id=foo', '--label', 'container_name=one', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['one']), '--detach=true', 'centos:7' ], mock.ANY), # run two mock.call([ 'docker', 'run', '--name', 'two-12345678', '--label', 'config_id=foo', '--label', 'container_name=two', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['two']), '--detach=true', 'centos:7' ], mock.ANY), # don't run three, its already running # run four mock.call([ 'docker', 'run', '--name', 'four-12345678', '--label', 'config_id=foo', '--label', 'container_name=four', '--label', 'managed_by=tester', '--label', 'config_data=%s' % json.dumps(config['four']), '--detach=true', 'centos:7' ], mock.ANY), # execute within four mock.call(['docker', 'exec', 'four-12345678', 'ls', '-l', '/'], mock.ANY), ])