From 4d34592f4a4e4c93a2bb4687c8adf3bdef5c850f Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 14 Feb 2017 13:34:42 +1300 Subject: [PATCH] Ensure unique container names When a container already exists with the desired name, the container name has a random suffix attached to it so that it can still be run. This ensures containers are always created regardless of other running containers. Since the name may not be as expected, the exec action needs an extra lookup to attempt to discover the actual name, falling back to the requested name if the lookup fails. Since there is a container_name label set with the desired name, the next patch in this series modifies 50-heat-config-docker-cmd to rename containers to their desired name when possible. Change-Id: Ibd97f52811f653295559d000487d2c50a7c67ece --- .../install.d/hook-docker-cmd.py | 61 ++- tests/test_hook_docker_cmd.py | 447 +++++++++++++----- 2 files changed, 392 insertions(+), 116 deletions(-) diff --git a/heat-config-docker-cmd/install.d/hook-docker-cmd.py b/heat-config-docker-cmd/install.d/hook-docker-cmd.py index 6285c04..9a285d7 100755 --- a/heat-config-docker-cmd/install.d/hook-docker-cmd.py +++ b/heat-config-docker-cmd/install.d/hook-docker-cmd.py @@ -15,6 +15,8 @@ import json import logging import os +import random +import string import subprocess import sys import yaml @@ -74,6 +76,54 @@ def label_arguments(cmd, container, cid, iv): ]) +def inspect(container, format=None): + cmd = [DOCKER_CMD, 'inspect'] + if format: + cmd.append('--format') + cmd.append(format) + cmd.append(container) + (cmd_stdout, cmd_stderr, returncode) = execute(cmd) + if returncode != 0: + return + try: + if format: + return cmd_stdout + else: + return json.loads(cmd_stdout)[0] + except Exception as e: + log.error('Problem parsing docker inspect: %s' % e) + + +def unique_container_name(container): + container_name = container + while inspect(container_name, format='exists'): + suffix = ''.join(random.choice( + string.ascii_lowercase + string.digits) for i in range(8)) + container_name = '%s-%s' % (container, suffix) + return container_name + + +def discover_container_name(container, cid): + cmd = [ + DOCKER_CMD, + 'ps', + '-a', + '--filter', + 'label=container_name=%s' % container, + '--filter', + 'label=config_id=%s' % cid, + '--format', + '{{.Names}}' + ] + (cmd_stdout, cmd_stderr, returncode) = execute(cmd) + if returncode != 0: + return container + names = cmd_stdout.split() + if names: + return names[0] + return container + + def main(argv=sys.argv): global log log = logging.getLogger('heat-config') @@ -119,7 +169,7 @@ def main(argv=sys.argv): DOCKER_CMD, 'run', '--name', - container + unique_container_name(container) ] label_arguments(cmd, container, c.get('id'), input_values) if config[container].get('detach', True): @@ -148,7 +198,14 @@ def main(argv=sys.argv): cmd.append(image_name) if 'command' in config[container]: - cmd.extend(config[container].get('command')) + command = config[container].get('command') + + if action == 'exec': + # for exec, the first argument is the container name, + # make sure the correct one is used + command[0] = discover_container_name(command[0], c.get('id')) + + cmd.extend(command) (cmd_stdout, cmd_stderr, returncode) = execute(cmd) if cmd_stdout: diff --git a/tests/test_hook_docker_cmd.py b/tests/test_hook_docker_cmd.py index 949073d..5a03b20 100644 --- a/tests/test_hook_docker_cmd.py +++ b/tests/test_hook_docker_cmd.py @@ -69,6 +69,7 @@ class HookDockerCmdTest(common.RunScriptTest): data_exit_code = { "name": "abcdef001", "group": "docker-cmd", + "id": "abc123", "config": { "web-ls": { "action": "exec", @@ -109,11 +110,293 @@ class HookDockerCmdTest(common.RunScriptTest): self.env.update({ 'TEST_RESPONSE': json.dumps([{ + 'stderr': 'Error: No such image, container or task: db', + 'returncode': 1 + }, { 'stdout': '', 'stderr': 'Creating db...' + }, { + 'stderr': 'Error: No such image, container or task: web', + 'returncode': 1 }, { 'stdout': '', 'stderr': 'Creating web...' + }, { + 'stdout': 'web', + }, { + + 'stdout': '', + 'stderr': 'one.txt\ntwo.txt\nthree.txt' + }]) + }) + returncode, stdout, stderr = self.run_cmd( + [self.hook_path], self.env, json.dumps(self.data)) + + self.assertEqual(0, returncode, stderr) + + self.assertEqual({ + 'deploy_stdout': '', + 'deploy_stderr': 'Creating db...\n' + 'Creating web...\n' + 'one.txt\ntwo.txt\nthree.txt', + 'deploy_status_code': 0 + }, json.loads(stdout)) + + state = list(self.json_from_files(self.test_state_path, 6)) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'db', + ], state[0]['args']) + self.assertEqual([ + self.fake_tool_path, + 'run', + '--name', + 'db', + '--label', + 'deploy_stack_id=the_stack', + '--label', + 'deploy_resource_name=the_deployment', + '--label', + 'config_id=abc123', + '--label', + 'container_name=db', + '--label', + 'managed_by=docker-cmd', + '--detach=true', + '--privileged=false', + 'xxx' + ], state[1]['args']) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'web', + ], state[2]['args']) + self.assertEqual([ + self.fake_tool_path, + 'run', + '--name', + 'web', + '--label', + 'deploy_stack_id=the_stack', + '--label', + 'deploy_resource_name=the_deployment', + '--label', + 'config_id=abc123', + '--label', + 'container_name=web', + '--label', + 'managed_by=docker-cmd', + '--detach=true', + '--env=KOLLA_CONFIG_STRATEGY=COPY_ALWAYS', + '--env=FOO=BAR', + '--net=host', + '--privileged=true', + '--restart=always', + '--user=root', + '--volume=/run:/run', + '--volume=db:/var/lib/db', + 'xxx' + ], state[3]['args']) + self.assertEqual([ + self.fake_tool_path, + 'ps', + '-a', + '--filter', + 'label=container_name=web', + '--filter', + 'label=config_id=abc123', + '--format', + '{{.Names}}', + ], state[4]['args']) + self.assertEqual([ + self.fake_tool_path, + 'exec', + 'web', + '/bin/ls', + '-l' + ], state[5]['args']) + + def test_hook_exit_codes(self): + + self.env.update({ + 'TEST_RESPONSE': json.dumps([{ + 'stdout': 'web', + }, { + 'stdout': '', + 'stderr': 'Warning: custom exit code', + 'returncode': 1 + }]) + }) + returncode, stdout, stderr = self.run_cmd( + [self.hook_path], self.env, json.dumps(self.data_exit_code)) + + self.assertEqual({ + 'deploy_stdout': '', + 'deploy_stderr': 'Warning: custom exit code', + 'deploy_status_code': 0 + }, json.loads(stdout)) + + state = list(self.json_from_files(self.test_state_path, 2)) + self.assertEqual([ + self.fake_tool_path, + 'ps', + '-a', + '--filter', + 'label=container_name=web', + '--filter', + 'label=config_id=abc123', + '--format', + '{{.Names}}', + ], state[0]['args']) + self.assertEqual([ + self.fake_tool_path, + 'exec', + 'web', + '/bin/ls', + '-l' + ], state[1]['args']) + + def test_hook_failed(self): + + self.env.update({ + 'TEST_RESPONSE': json.dumps([{ + 'stderr': 'Error: No such image, container or task: db', + 'returncode': 1 + }, { + 'stdout': '', + 'stderr': 'Creating db...' + }, { + 'stderr': 'Error: No such image, container or task: web', + 'returncode': 1 + }, { + 'stdout': '', + 'stderr': 'Creating web...' + }, { + 'stdout': 'web', + }, { + 'stdout': '', + 'stderr': 'No such file or directory', + 'returncode': 2 + }]) + }) + returncode, stdout, stderr = self.run_cmd( + [self.hook_path], self.env, json.dumps(self.data)) + + self.assertEqual({ + 'deploy_stdout': '', + 'deploy_stderr': 'Creating db...\n' + 'Creating web...\n' + 'No such file or directory', + 'deploy_status_code': 2 + }, json.loads(stdout)) + + state = list(self.json_from_files(self.test_state_path, 6)) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'db', + ], state[0]['args']) + self.assertEqual([ + self.fake_tool_path, + 'run', + '--name', + 'db', + '--label', + 'deploy_stack_id=the_stack', + '--label', + 'deploy_resource_name=the_deployment', + '--label', + 'config_id=abc123', + '--label', + 'container_name=db', + '--label', + 'managed_by=docker-cmd', + '--detach=true', + '--privileged=false', + 'xxx' + ], state[1]['args']) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'web', + ], state[2]['args']) + self.assertEqual([ + self.fake_tool_path, + 'run', + '--name', + 'web', + '--label', + 'deploy_stack_id=the_stack', + '--label', + 'deploy_resource_name=the_deployment', + '--label', + 'config_id=abc123', + '--label', + 'container_name=web', + '--label', + 'managed_by=docker-cmd', + '--detach=true', + '--env=KOLLA_CONFIG_STRATEGY=COPY_ALWAYS', + '--env=FOO=BAR', + '--net=host', + '--privileged=true', + '--restart=always', + '--user=root', + '--volume=/run:/run', + '--volume=db:/var/lib/db', + 'xxx' + ], state[3]['args']) + self.assertEqual([ + self.fake_tool_path, + 'ps', + '-a', + '--filter', + 'label=container_name=web', + '--filter', + 'label=config_id=abc123', + '--format', + '{{.Names}}', + ], state[4]['args']) + self.assertEqual([ + self.fake_tool_path, + 'exec', + 'web', + '/bin/ls', + '-l' + ], state[5]['args']) + + def test_hook_unique_names(self): + + self.env.update({ + 'TEST_RESPONSE': json.dumps([{ + 'stdout': 'exists\n', + 'returncode': 0 + }, { + 'stderr': 'Error: No such image, container or task: db-blah', + 'returncode': 1 + }, { + 'stdout': '', + 'stderr': 'Creating db...' + }, { + 'stdout': 'exists\n', + 'returncode': 0 + }, { + 'stderr': 'Error: No such image, container or task: web-blah', + 'returncode': 1 + }, { + 'stdout': '', + 'stderr': 'Creating web...' + }, { + 'stdout': 'web-asdf1234', }, { 'stdout': '', 'stderr': 'one.txt\ntwo.txt\nthree.txt' @@ -132,12 +415,30 @@ class HookDockerCmdTest(common.RunScriptTest): 'deploy_status_code': 0 }, json.loads(stdout)) - state = list(self.json_from_files(self.test_state_path, 3)) + state = list(self.json_from_files(self.test_state_path, 8)) + db_container_name = state[1]['args'][4] + web_container_name = state[4]['args'][4] + self.assertRegex(db_container_name, 'db-[0-9a-z]{8}') + self.assertRegex(web_container_name, 'web-[0-9a-z]{8}') + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'db', + ], state[0]['args']) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + db_container_name, + ], state[1]['args']) self.assertEqual([ self.fake_tool_path, 'run', '--name', - 'db', + db_container_name, '--label', 'deploy_stack_id=the_stack', '--label', @@ -151,12 +452,26 @@ class HookDockerCmdTest(common.RunScriptTest): '--detach=true', '--privileged=false', 'xxx' - ], state[0]['args']) + ], state[2]['args']) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + 'web', + ], state[3]['args']) + self.assertEqual([ + self.fake_tool_path, + 'inspect', + '--format', + 'exists', + web_container_name, + ], state[4]['args']) self.assertEqual([ self.fake_tool_path, 'run', '--name', - 'web', + web_container_name, '--label', 'deploy_stack_id=the_stack', '--label', @@ -177,121 +492,25 @@ class HookDockerCmdTest(common.RunScriptTest): '--volume=/run:/run', '--volume=db:/var/lib/db', 'xxx' - ], state[1]['args']) + ], state[5]['args']) + self.assertEqual([ + self.fake_tool_path, + 'ps', + '-a', + '--filter', + 'label=container_name=web', + '--filter', + 'label=config_id=abc123', + '--format', + '{{.Names}}', + ], state[6]['args']) self.assertEqual([ self.fake_tool_path, 'exec', - 'web', + 'web-asdf1234', '/bin/ls', '-l' - ], state[2]['args']) - - def test_hook_exit_codes(self): - - self.env.update({ - 'TEST_RESPONSE': json.dumps({ - 'stdout': '', - 'stderr': 'Warning: custom exit code', - 'returncode': 1 - }) - }) - returncode, stdout, stderr = self.run_cmd( - [self.hook_path], self.env, json.dumps(self.data_exit_code)) - - self.assertEqual({ - 'deploy_stdout': '', - 'deploy_stderr': 'Warning: custom exit code', - 'deploy_status_code': 0 - }, json.loads(stdout)) - - state = list(self.json_from_files(self.test_state_path, 1)) - self.assertEqual([ - self.fake_tool_path, - 'exec', - 'web', - '/bin/ls', - '-l' - ], state[0]['args']) - - def test_hook_failed(self): - - self.env.update({ - 'TEST_RESPONSE': json.dumps([{ - 'stdout': '', - 'stderr': 'Creating db...' - }, { - 'stdout': '', - 'stderr': 'Creating web...' - }, { - 'stdout': '', - 'stderr': 'No such file or directory', - 'returncode': 2 - }]) - }) - returncode, stdout, stderr = self.run_cmd( - [self.hook_path], self.env, json.dumps(self.data)) - - self.assertEqual({ - 'deploy_stdout': '', - 'deploy_stderr': 'Creating db...\n' - 'Creating web...\n' - 'No such file or directory', - 'deploy_status_code': 2 - }, json.loads(stdout)) - - state = list(self.json_from_files(self.test_state_path, 3)) - self.assertEqual([ - self.fake_tool_path, - 'run', - '--name', - 'db', - '--label', - 'deploy_stack_id=the_stack', - '--label', - 'deploy_resource_name=the_deployment', - '--label', - 'config_id=abc123', - '--label', - 'container_name=db', - '--label', - 'managed_by=docker-cmd', - '--detach=true', - '--privileged=false', - 'xxx' - ], state[0]['args']) - self.assertEqual([ - self.fake_tool_path, - 'run', - '--name', - 'web', - '--label', - 'deploy_stack_id=the_stack', - '--label', - 'deploy_resource_name=the_deployment', - '--label', - 'config_id=abc123', - '--label', - 'container_name=web', - '--label', - 'managed_by=docker-cmd', - '--detach=true', - '--env=KOLLA_CONFIG_STRATEGY=COPY_ALWAYS', - '--env=FOO=BAR', - '--net=host', - '--privileged=true', - '--restart=always', - '--user=root', - '--volume=/run:/run', - '--volume=db:/var/lib/db', - 'xxx' - ], state[1]['args']) - self.assertEqual([ - self.fake_tool_path, - 'exec', - 'web', - '/bin/ls', - '-l' - ], state[2]['args']) + ], state[7]['args']) def test_cleanup_deleted(self): self.env.update({