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
This commit is contained in:
Steve Baker 2017-02-14 13:34:42 +13:00
parent b6dfdf8e99
commit 4d34592f4a
2 changed files with 392 additions and 116 deletions

View File

@ -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:

View File

@ -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({