Expose recent docker features in the resource

This adds the name, port_bindings and links properties to the docker
container resource which are passed to container creation and start.
This also removes properties which were never implemented.

Change-Id: I7320e3a95895b894cc53ebb77ebb220482f990c1
This commit is contained in:
Thomas Herve 2014-07-07 17:49:57 +02:00
parent 54a90a3cf6
commit 6906fd12b6
3 changed files with 103 additions and 55 deletions

View File

@ -35,15 +35,13 @@ except ImportError:
class DockerContainer(resource.Resource): class DockerContainer(resource.Resource):
PROPERTIES = ( PROPERTIES = (
DOCKER_ENDPOINT, HOSTNAME, USER, MEMORY, ATTACH_STDIN, DOCKER_ENDPOINT, HOSTNAME, USER, MEMORY, PORT_SPECS,
ATTACH_STDOUT, ATTACH_STDERR, PORT_SPECS, PRIVILEGED, TTY, PRIVILEGED, TTY, OPEN_STDIN, STDIN_ONCE, ENV, CMD, DNS,
OPEN_STDIN, STDIN_ONCE, ENV, CMD, DNS, IMAGE, VOLUMES, IMAGE, VOLUMES, VOLUMES_FROM, PORT_BINDINGS, LINKS, NAME,
VOLUMES_FROM,
) = ( ) = (
'docker_endpoint', 'hostname', 'user', 'memory', 'attach_stdin', 'docker_endpoint', 'hostname', 'user', 'memory', 'port_specs',
'attach_stdout', 'attach_stderr', 'port_specs', 'privileged', 'tty', 'privileged', 'tty', 'open_stdin', 'stdin_once', 'env', 'cmd', 'dns',
'open_stdin', 'stdin_once', 'env', 'cmd', 'dns', 'image', 'volumes', 'image', 'volumes', 'volumes_from', 'port_bindings', 'links', 'name'
'volumes_from',
) )
ATTRIBUTES = ( ATTRIBUTES = (
@ -60,122 +58,117 @@ class DockerContainer(resource.Resource):
DOCKER_ENDPOINT: properties.Schema( DOCKER_ENDPOINT: properties.Schema(
properties.Schema.STRING, properties.Schema.STRING,
_('Docker daemon endpoint (by default the local docker daemon ' _('Docker daemon endpoint (by default the local docker daemon '
'will be used)'), 'will be used).'),
default=None default=None
), ),
HOSTNAME: properties.Schema( HOSTNAME: properties.Schema(
properties.Schema.STRING, properties.Schema.STRING,
_('Hostname of the container'), _('Hostname of the container.'),
default='' default=''
), ),
USER: properties.Schema( USER: properties.Schema(
properties.Schema.STRING, properties.Schema.STRING,
_('Username or UID'), _('Username or UID.'),
default='' default=''
), ),
MEMORY: properties.Schema( MEMORY: properties.Schema(
properties.Schema.INTEGER, properties.Schema.INTEGER,
_('Memory limit (Bytes)'), _('Memory limit (Bytes).'),
default=0 default=0
), ),
ATTACH_STDIN: properties.Schema(
properties.Schema.BOOLEAN,
_('Attach to the process\' standard input'),
default=False
),
ATTACH_STDOUT: properties.Schema(
properties.Schema.BOOLEAN,
_('Attach to the process\' standard output'),
default=True
),
ATTACH_STDERR: properties.Schema(
properties.Schema.BOOLEAN,
_('Attach to the process\' standard error'),
default=True
),
PORT_SPECS: properties.Schema( PORT_SPECS: properties.Schema(
properties.Schema.LIST, properties.Schema.LIST,
_('TCP/UDP ports mapping'), _('TCP/UDP ports mapping.'),
default=None default=None
), ),
PORT_BINDINGS: properties.Schema(
properties.Schema.MAP,
_('TCP/UDP ports bindings.'),
),
LINKS: properties.Schema(
properties.Schema.MAP,
_('Links to other containers.'),
),
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the container.'),
),
PRIVILEGED: properties.Schema( PRIVILEGED: properties.Schema(
properties.Schema.BOOLEAN, properties.Schema.BOOLEAN,
_('Enable extended privileges'), _('Enable extended privileges.'),
default=False default=False
), ),
TTY: properties.Schema( TTY: properties.Schema(
properties.Schema.BOOLEAN, properties.Schema.BOOLEAN,
_('Allocate a pseudo-tty'), _('Allocate a pseudo-tty.'),
default=False default=False
), ),
OPEN_STDIN: properties.Schema( OPEN_STDIN: properties.Schema(
properties.Schema.BOOLEAN, properties.Schema.BOOLEAN,
_('Open stdin'), _('Open stdin.'),
default=False default=False
), ),
STDIN_ONCE: properties.Schema( STDIN_ONCE: properties.Schema(
properties.Schema.BOOLEAN, properties.Schema.BOOLEAN,
_('If true, close stdin after the 1 attached client disconnects'), _('If true, close stdin after the 1 attached client disconnects.'),
default=False default=False
), ),
ENV: properties.Schema( ENV: properties.Schema(
properties.Schema.LIST, properties.Schema.LIST,
_('Set environment variables'), _('Set environment variables.'),
default=None
), ),
CMD: properties.Schema( CMD: properties.Schema(
properties.Schema.LIST, properties.Schema.LIST,
_('Command to run after spawning the container'), _('Command to run after spawning the container.'),
default=[] default=[]
), ),
DNS: properties.Schema( DNS: properties.Schema(
properties.Schema.LIST, properties.Schema.LIST,
_('Set custom dns servers'), _('Set custom dns servers.'),
default=None
), ),
IMAGE: properties.Schema( IMAGE: properties.Schema(
properties.Schema.STRING, properties.Schema.STRING,
_('Image name') _('Image name.')
), ),
VOLUMES: properties.Schema( VOLUMES: properties.Schema(
properties.Schema.MAP, properties.Schema.MAP,
_('Create a bind mount'), _('Create a bind mount.'),
default={} default={}
), ),
VOLUMES_FROM: properties.Schema( VOLUMES_FROM: properties.Schema(
properties.Schema.STRING, properties.Schema.STRING,
_('Mount all specified volumes'), _('Mount all specified volumes.'),
default='' default=''
), ),
} }
attributes_schema = { attributes_schema = {
INFO: attributes.Schema( INFO: attributes.Schema(
_('Container info') _('Container info.')
), ),
NETWORK_INFO: attributes.Schema( NETWORK_INFO: attributes.Schema(
_('Container network info') _('Container network info.')
), ),
NETWORK_IP: attributes.Schema( NETWORK_IP: attributes.Schema(
_('Container ip address') _('Container ip address.')
), ),
NETWORK_GATEWAY: attributes.Schema( NETWORK_GATEWAY: attributes.Schema(
_('Container ip gateway') _('Container ip gateway.')
), ),
NETWORK_TCP_PORTS: attributes.Schema( NETWORK_TCP_PORTS: attributes.Schema(
_('Container TCP ports') _('Container TCP ports.')
), ),
NETWORK_UDP_PORTS: attributes.Schema( NETWORK_UDP_PORTS: attributes.Schema(
_('Container UDP ports') _('Container UDP ports.')
), ),
LOGS: attributes.Schema( LOGS: attributes.Schema(
_('Container logs') _('Container logs.')
), ),
LOGS_HEAD: attributes.Schema( LOGS_HEAD: attributes.Schema(
_('Container first logs line') _('Container first logs line.')
), ),
LOGS_TAIL: attributes.Schema( LOGS_TAIL: attributes.Schema(
_('Container last logs line') _('Container last logs line.')
), ),
} }
@ -264,8 +257,10 @@ class DockerContainer(resource.Resource):
'dns': self.properties[self.DNS], 'dns': self.properties[self.DNS],
'volumes': self.properties[self.VOLUMES], 'volumes': self.properties[self.VOLUMES],
'volumes_from': self.properties[self.VOLUMES_FROM], 'volumes_from': self.properties[self.VOLUMES_FROM],
'name': self.properties[self.NAME]
} }
client = self.get_client() client = self.get_client()
client.pull(self.properties[self.IMAGE])
result = client.create_container(**args) result = client.create_container(**args)
container_id = result['Id'] container_id = result['Id']
self.resource_id_set(container_id) self.resource_id_set(container_id)
@ -275,6 +270,10 @@ class DockerContainer(resource.Resource):
kwargs[self.PRIVILEGED] = True kwargs[self.PRIVILEGED] = True
if self.properties[self.VOLUMES]: if self.properties[self.VOLUMES]:
kwargs['binds'] = self.properties[self.VOLUMES] kwargs['binds'] = self.properties[self.VOLUMES]
if self.properties[self.PORT_BINDINGS]:
kwargs['port_bindings'] = self.properties[self.PORT_BINDINGS]
if self.properties[self.LINKS]:
kwargs['links'] = self.properties[self.LINKS]
client.start(container_id, **kwargs) client.start(container_id, **kwargs)
return container_id return container_id

View File

@ -27,6 +27,9 @@ class FakeDockerClient(object):
def __init__(self, endpoint=None): def __init__(self, endpoint=None):
self._endpoint = endpoint self._endpoint = endpoint
self._containers = {} self._containers = {}
self.pulled_images = []
self.container_create = []
self.container_start = []
def _generate_string(self, n=32): def _generate_string(self, n=32):
return ''.join(random.choice(string.lowercase) for i in range(n)) return ''.join(random.choice(string.lowercase) for i in range(n))
@ -66,13 +69,15 @@ class FakeDockerClient(object):
logs.append('---logs_end---') logs.append('---logs_end---')
return '\n'.join(logs) return '\n'.join(logs)
def create_container(self, *args, **kwargs): def create_container(self, **kwargs):
self.container_create.append(kwargs)
container_id = self._generate_string() container_id = self._generate_string()
self._containers[container_id] = None self._containers[container_id] = None
self._set_running(container_id, False) self._set_running(container_id, False)
return self.inspect_container(container_id) return self.inspect_container(container_id)
def start(self, container_id, privileged=None): def start(self, container_id, **kwargs):
self.container_start.append(kwargs)
self._set_running(container_id, True) self._set_running(container_id, True)
def stop(self, container_id): def stop(self, container_id):
@ -80,3 +85,6 @@ class FakeDockerClient(object):
def kill(self, container_id): def kill(self, container_id):
self._set_running(container_id, False) self._set_running(container_id, False)
def pull(self, image):
self.pulled_images.append(image)

View File

@ -51,6 +51,7 @@ class DockerContainerTest(HeatTestCase):
super(DockerContainerTest, self).setUp() super(DockerContainerTest, self).setUp()
for res_name, res_class in docker_container.resource_mapping().items(): for res_name, res_class in docker_container.resource_mapping().items():
resource._register_class(res_name, res_class) resource._register_class(res_name, res_class)
self.addCleanup(self.m.VerifyAll)
def create_container(self, resource_name): def create_container(self, resource_name):
t = template_format.parse(template) t = template_format.parse(template)
@ -76,7 +77,50 @@ class DockerContainerTest(HeatTestCase):
self.assertTrue(container.resource_id) self.assertTrue(container.resource_id)
running = self.get_container_state(container)['Running'] running = self.get_container_state(container)['Running']
self.assertIs(True, running) self.assertIs(True, running)
self.m.VerifyAll() client = container.get_client()
self.assertEqual(['samalba/wordpress'], client.pulled_images)
self.assertIsNone(client.container_create[0]['name'])
def test_create_with_name(self):
t = template_format.parse(template)
stack = utils.parse_stack(t)
definition = stack.t.resource_definitions(stack)['Blog']
definition['Properties']['name'] = 'super-blog'
resource = docker_container.DockerContainer(
'Blog', definition, stack)
self.m.StubOutWithMock(resource, 'get_client')
resource.get_client().MultipleTimes().AndReturn(FakeDockerClient())
self.assertIsNone(resource.validate())
self.m.ReplayAll()
scheduler.TaskRunner(resource.create)()
self.assertEqual((resource.CREATE, resource.COMPLETE),
resource.state)
client = resource.get_client()
self.assertEqual(['samalba/wordpress'], client.pulled_images)
self.assertEqual('super-blog', client.container_create[0]['name'])
def test_start_with_bindings_and_links(self):
t = template_format.parse(template)
stack = utils.parse_stack(t)
definition = stack.t.resource_definitions(stack)['Blog']
definition['Properties']['port_bindings'] = {
'80/tcp': [{'HostPort': '80'}]}
definition['Properties']['links'] = {'db': 'mysql'}
resource = docker_container.DockerContainer(
'Blog', definition, stack)
self.m.StubOutWithMock(resource, 'get_client')
resource.get_client().MultipleTimes().AndReturn(FakeDockerClient())
self.assertIsNone(resource.validate())
self.m.ReplayAll()
scheduler.TaskRunner(resource.create)()
self.assertEqual((resource.CREATE, resource.COMPLETE),
resource.state)
client = resource.get_client()
self.assertEqual(['samalba/wordpress'], client.pulled_images)
self.assertEqual({'db': 'mysql'}, client.container_start[0]['links'])
self.assertEqual(
{'80/tcp': [{'HostPort': '80'}]},
client.container_start[0]['port_bindings'])
def test_resource_attributes(self): def test_resource_attributes(self):
container = self.create_container('Blog') container = self.create_container('Blog')
@ -91,7 +135,6 @@ class DockerContainerTest(HeatTestCase):
# Test a non existing attribute # Test a non existing attribute
self.assertRaises(exception.InvalidTemplateAttribute, self.assertRaises(exception.InvalidTemplateAttribute,
container.FnGetAtt, 'invalid_attribute') container.FnGetAtt, 'invalid_attribute')
self.m.VerifyAll()
def test_resource_delete(self): def test_resource_delete(self):
container = self.create_container('Blog') container = self.create_container('Blog')
@ -100,7 +143,6 @@ class DockerContainerTest(HeatTestCase):
container.state) container.state)
running = self.get_container_state(container)['Running'] running = self.get_container_state(container)['Running']
self.assertIs(False, running) self.assertIs(False, running)
self.m.VerifyAll()
def test_resource_suspend_resume(self): def test_resource_suspend_resume(self):
container = self.create_container('Blog') container = self.create_container('Blog')
@ -116,4 +158,3 @@ class DockerContainerTest(HeatTestCase):
container.state) container.state)
running = self.get_container_state(container)['Running'] running = self.get_container_state(container)['Running']
self.assertIs(True, running) self.assertIs(True, running)
self.m.VerifyAll()