Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FLOC-3767] Container volume support in API client #2388

Merged
merged 6 commits into from
Dec 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not really related to this PR. But setting the version of a UUID only makes sense if we are minting a new UUID from some data. Overriding the version when creating a UUID instance from existing UUID data is asking for trouble.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

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