Skip to content

Commit

Permalink
Merge pull request #2388 from ClusterHQ/apiclient-container-volumes-F…
Browse files Browse the repository at this point in the history
…LOC-3767

[FLOC-3767] Container volume support in API client
  • Loading branch information
jongiddy committed Dec 31, 2015
2 parents d2bef1a + f6925c0 commit 43114d4
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 9 deletions.
66 changes: 58 additions & 8 deletions flocker/apiclient/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,34 +111,71 @@ class Lease(PClass):
expires = field(type=(float, int, NoneType), mandatory=True)


class MountedDataset(PClass):
"""
A mounted dataset.
:attr UUID dataset_id: The UUID of the dataset.
:attr unicode mountpoint: The filesystem location of the dataset.
"""
dataset_id = field(type=UUID, mandatory=True)
mountpoint = field(type=unicode, mandatory=True)


def _parse_volumes(data_list):
"""
Parse a list of volume configuration.
:param Optional[Sequence[Mapping[unicode, unicode]]] data_list: Sequence
of data describing volume objects.
:return Optional[Sequence[MountedDataset]]: Sequence of mounted datasets,
or None if no volumes.
"""
if data_list:
return [
MountedDataset(
dataset_id=UUID(data[u'dataset_id']),
mountpoint=data[u'mountpoint'],
) for data in data_list
]
else:
return None


class Container(PClass):
"""
A container in the configuration.
:attr UUID node_uuid: The UUID of a node in the cluster where the container
will run.
:attr UUID node_uuid: The UUID of a node in the cluster where the
container will run.
:attr unicode name: The unique name of the container.
:attr DockerImage image: The Docker image the container will run.
:attr Optional[Sequence[MountedDataset]] volumes: Flocker volumes
mounted in container.
"""
node_uuid = field(type=UUID, mandatory=True)
name = field(type=unicode, mandatory=True)
image = field(type=DockerImage, mandatory=True)
volumes = field(initial=None)


class ContainerState(PClass):
"""
The state of a container in the cluster.
:attr UUID node_uuid: The UUID of a node in the cluster where the container
will run.
:attr UUID node_uuid: The UUID of a node in the cluster where the
container will run.
:attr unicode name: The unique name of the container.
:attr DockerImage image: The name of the Docker image.
:attr bool running: Whether the container is running.
:attr Optional[Sequence[MountedDataset]] volumes: Flocker volumes
mounted in container.
"""
node_uuid = field(type=UUID, mandatory=True)
name = field(type=unicode, mandatory=True)
image = field(type=DockerImage, mandatory=True)
running = field(type=bool, mandatory=True)
volumes = field(initial=None, mandatory=True)


class Node(PClass):
Expand Down Expand Up @@ -315,13 +352,15 @@ def list_nodes():
:return: ``Deferred`` firing with a ``list`` of ``Node``.
"""

def create_container(node_uuid, name, image):
def create_container(node_uuid, name, image, volumes=None):
"""
:param UUID node_uuid: The ``UUID`` of the node where the container
will be started.
:param unicode name: The name to assign to the container.
:param DockerImage image: The Docker image which the container will
run.
:param Optional[Sequence[MountedDataset]] volumes: Volumes to mount on
container.
:return: ``Deferred`` firing with the configured ``Container`` or
``ContainerAlreadyExists`` if the supplied container name already
Expand Down Expand Up @@ -450,6 +489,7 @@ def synchronize_state(self):
name=container.name,
image=container.image,
running=True,
volumes=container.volumes,
) for container in self._configured_containers.values()
]

Expand Down Expand Up @@ -488,13 +528,14 @@ def version(self):
def list_nodes(self):
return succeed(self._nodes)

def create_container(self, node_uuid, name, image):
def create_container(self, node_uuid, name, image, volumes=None):
if name in self._configured_containers:
return fail(ContainerAlreadyExists())
result = Container(
node_uuid=node_uuid,
name=name,
image=image,
volumes=volumes,
)
self._configured_containers = self._configured_containers.set(
name, result
Expand Down Expand Up @@ -760,15 +801,23 @@ def _parse_configuration_container(self, container_dict):
:return: ``Container`` instance.
"""
return Container(
node_uuid=UUID(hex=container_dict[u"node_uuid"], version=4),
node_uuid=UUID(hex=container_dict[u"node_uuid"]),
name=container_dict[u'name'],
image=DockerImage.from_string(container_dict[u"image"]),
volumes=_parse_volumes(container_dict.get(u'volumes')),
)

def create_container(self, node_uuid, name, image):
def create_container(self, node_uuid, name, image, volumes=None):
container = dict(
node_uuid=unicode(node_uuid), name=name, image=image.full_name,
)
if volumes:
container[u'volumes'] = [
{
u'dataset_id': unicode(volume.dataset_id),
u'mountpoint': volume.mountpoint
} for volume in volumes
]
d = self._request(
b"POST",
b"/configuration/containers",
Expand Down Expand Up @@ -799,6 +848,7 @@ def parse(container):
name=container[u'name'],
image=DockerImage.from_string(container[u'image']),
running=container[u'running'],
volumes=_parse_volumes(container.get(u'volumes'))
)
except KeyError as e:
raise ServerResponseMissingElementError(e.args[0], container)
Expand Down
70 changes: 69 additions & 1 deletion flocker/apiclient/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
DatasetState, FlockerClient, ResponseError, _LOG_HTTP_REQUEST,
Lease, LeaseAlreadyHeld, Node, Container, ContainerAlreadyExists,
DatasetsConfiguration, ConfigurationChanged, conditional_create,
_LOG_CONDITIONAL_CREATE, ContainerState,
_LOG_CONDITIONAL_CREATE, ContainerState, MountedDataset,
)
from ...ca import rest_api_context_factory
from ...ca.testtools import get_credential_sets
Expand Down Expand Up @@ -520,6 +520,74 @@ def test_container_state(self):

return d

def test_container_volumes(self):
"""
Mounted datasets are included in response messages.
"""
d = self.assert_creates(
self.client, primary=self.node_1.uuid,
maximum_size=DATASET_SIZE
)

def start_container(dataset):
name = random_name(case=self)
volumes = [
MountedDataset(
dataset_id=dataset.dataset_id, mountpoint=u'/data')
]
expected_configuration = Container(
node_uuid=self.node_1.uuid,
name=name,
image=DockerImage.from_string(u'nginx'),
volumes=volumes,
)

# Create a container with an attached dataset
d = self.client.create_container(
node_uuid=expected_configuration.node_uuid,
name=expected_configuration.name,
image=expected_configuration.image,
volumes=expected_configuration.volumes,
)

# Result of create call is stateful container configuration
d.addCallback(
lambda configuration: self.assertEqual(
configuration, expected_configuration
)
)

# Cluster configuration contains stateful container
d.addCallback(
lambda _ignore: self.client.list_containers_configuration()
).addCallback(
lambda configurations: self.assertIn(
expected_configuration, configurations
)
)

d.addCallback(lambda _ignore: self.synchronize_state())

expected_state = ContainerState(
node_uuid=self.node_1.uuid,
name=name,
image=DockerImage.from_string(u'nginx'),
running=True,
volumes=volumes,
)

# After convergence, cluster state contains stateful container
d.addCallback(
lambda _ignore: self.client.list_containers_state()
).addCallback(
lambda states: self.assertIn(expected_state, states)
)

return d
d.addCallback(start_container)

return d

def test_delete_container(self):
"""
``delete_container`` returns a deferred that fires with ``None``.
Expand Down

0 comments on commit 43114d4

Please sign in to comment.