Merge lp:~johnsca/charm-helpers/docker into lp:charm-helpers

Proposed by Cory Johns
Status: Needs review
Proposed branch: lp:~johnsca/charm-helpers/docker
Merge into: lp:charm-helpers
Diff against target: 627 lines (+552/-1)
7 files modified
charmhelpers/contrib/docker/__init__.py (+287/-0)
charmhelpers/core/host.py (+5/-0)
charmhelpers/core/services/base.py (+14/-1)
docs/api/charmhelpers.contrib.docker.rst (+8/-0)
docs/api/charmhelpers.contrib.rst (+1/-0)
docs/examples/docker.rst (+119/-0)
tests/contrib/docker/test_docker.py (+118/-0)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/docker
Reviewer Review Type Date Requested Status
amir sanjar (community) Approve
Benjamin Saller (community) Approve
charmers Pending
Charm Helper Maintainers Pending
Review via email: mp+231226@code.launchpad.net

Description of the change

Added helpers for managing Docker containers in a charm, using the services framework. Includes documentation.

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (25.0 KiB)

i'd also appreciate an option for docker install latest ala
https://docs.docker.com/installation/binaries/

On Mon, Aug 18, 2014 at 12:29 PM, Cory Johns <email address hidden>
wrote:

> Cory Johns has proposed merging lp:~johnsca/charm-helpers/docker into
> lp:charm-helpers.
>
> Requested reviews:
> Charm Helper Maintainers (charm-helpers)
>
> For more details, see:
> https://code.launchpad.net/~johnsca/charm-helpers/docker/+merge/231226
>
> Added helpers for managing Docker containers in a charm, using the
> services framework. Includes documentation.
> --
> https://code.launchpad.net/~johnsca/charm-helpers/docker/+merge/231226
> Your team Charm Helper Maintainers is requested to review the proposed
> merge of lp:~johnsca/charm-helpers/docker into lp:charm-helpers.
>
> === added directory 'charmhelpers/contrib/docker'
> === added file 'charmhelpers/contrib/docker/__init__.py'
> --- charmhelpers/contrib/docker/__init__.py 1970-01-01 00:00:00 +0000
> +++ charmhelpers/contrib/docker/__init__.py 2014-08-18 16:28:55 +0000
> @@ -0,0 +1,287 @@
> +
> +import os
> +import subprocess
> +
> +from charmhelpers import fetch
> +from charmhelpers.core import host
> +from charmhelpers.core import hookenv
> +from charmhelpers.core.services.base import ManagerCallback
> +from charmhelpers.core.services.helpers import RelationContext
> +
> +
> +def install_docker():
> + """
> + Install the Docker tools.
> + """
> + fetch.apt_install(['docker.io'])
> + if os.path.exists('/usr/local/bin/docker'):
> + os.unlink('/usr/local/bin/docker')
> + os.symlink('/usr/bin/docker.io', '/usr/local/bin/docker')
> + with open('/etc/bash_completion.d/docker.io', 'a') as fp:
> + fp.write('\ncomplete -F _docker docker')
> +
> +
> +def install_docker_unstable():
> + """
> + Install the Docker tools from the unstable (but potentially more
> + up-to-date) repository.
> + """
> + fetch.add_source('deb https://get.docker.io/ubuntu docker main',
> + key='36A1D7869245C8950F966E92D8576A8BA88D21E9')
> + fetch.apt_update(fatal=True)
> + fetch.apt_install(['lxc-docker'])
> +
> +
> +def docker_pull(container_name, registry=None, username=None,
> password=None):
> + """
> + Fetch a container from the Docker registry.
> +
> + If :param:`registry` is given, the container will be pulled from that
> + registry instead of the default. If the registry requires
> authentication,
> + provide the :param:`username` and :param:`password` arguments.
> + """
> + if registry is not None:
> + container_name = '/'.join([registry, container_name])
> + if any(username, password):
> + subprocess.check_call([
> + 'docker', 'login', '-u', username, '-p', password])
> + subprocess.check_call(['docker', 'pull', container_name])
> +
> +
> +class DockerCallback(ManagerCallback):
> + """
> + ServiceManager callback to manage starting up a Docker container.
> +
> + Can be referenced as ``docker_start`` or ``docker_stop``, and performs
> + the appropriate action. Requires one or more of
> + :class:`DockerPortMappings`, :class:`DockerVolumes`,
> + :c...

Revision history for this message
Cory Johns (johnsca) wrote :

> i'd also appreciate an option for docker install latest ala
> https://docs.docker.com/installation/binaries/

Their instructions for installing the latest version on Ubuntu say to use their apt repo: https://docs.docker.com/installation/ubuntulinux/

You think there's likely to be drift between their apt repo version and the latest binary to warrant installing the binaries manually?

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

fair enough.. i was considering cross platform and the way i always install
it (static bin dl), in addition to the oncoming pkg binary name change
(though it will be backwards compatible). but what your doing is good given
the XP is theoretical at this point.

On Tue, Aug 19, 2014 at 10:05 AM, Cory Johns <email address hidden>
wrote:

> > i'd also appreciate an option for docker install latest ala
> > https://docs.docker.com/installation/binaries/
>
> Their instructions for installing the latest version on Ubuntu say to use
> their apt repo: https://docs.docker.com/installation/ubuntulinux/
>
> You think there's likely to be drift between their apt repo version and
> the latest binary to warrant installing the binaries manually?
> --
> https://code.launchpad.net/~johnsca/charm-helpers/docker/+merge/231226
> Your team Charm Helper Maintainers is requested to review the proposed
> merge of lp:~johnsca/charm-helpers/docker into lp:charm-helpers.
>

Revision history for this message
Cory Johns (johnsca) wrote :

> fair enough.. i was considering cross platform and the way i always install
> it (static bin dl), in addition to the oncoming pkg binary name change
> (though it will be backwards compatible). but what your doing is good given
> the XP is theoretical at this point.

And since this is a helper, it will be easy enough to refactor it later as long as the API doesn't change (which is just a no-arg function call).

Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM, thanks, Good Work.

review: Approve
Revision history for this message
amir sanjar (asanjar) wrote :

+1
As much as personally I don't think this should matter anymore, but charmers have frown at using
path as follow:
if os.path.exists('/usr/local/bin/docker'):
   os.unlink('/usr/local/bin/docker')

recommend to change to:
docker_path = os.path.join(os.path.sep, 'usr', 'local','bin','docker')

review: Approve

Unmerged revisions

195. By Cory Johns

Added Docker helpers and documentation

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'charmhelpers/contrib/docker'
2=== added file 'charmhelpers/contrib/docker/__init__.py'
3--- charmhelpers/contrib/docker/__init__.py 1970-01-01 00:00:00 +0000
4+++ charmhelpers/contrib/docker/__init__.py 2014-08-18 16:28:55 +0000
5@@ -0,0 +1,287 @@
6+
7+import os
8+import subprocess
9+
10+from charmhelpers import fetch
11+from charmhelpers.core import host
12+from charmhelpers.core import hookenv
13+from charmhelpers.core.services.base import ManagerCallback
14+from charmhelpers.core.services.helpers import RelationContext
15+
16+
17+def install_docker():
18+ """
19+ Install the Docker tools.
20+ """
21+ fetch.apt_install(['docker.io'])
22+ if os.path.exists('/usr/local/bin/docker'):
23+ os.unlink('/usr/local/bin/docker')
24+ os.symlink('/usr/bin/docker.io', '/usr/local/bin/docker')
25+ with open('/etc/bash_completion.d/docker.io', 'a') as fp:
26+ fp.write('\ncomplete -F _docker docker')
27+
28+
29+def install_docker_unstable():
30+ """
31+ Install the Docker tools from the unstable (but potentially more
32+ up-to-date) repository.
33+ """
34+ fetch.add_source('deb https://get.docker.io/ubuntu docker main',
35+ key='36A1D7869245C8950F966E92D8576A8BA88D21E9')
36+ fetch.apt_update(fatal=True)
37+ fetch.apt_install(['lxc-docker'])
38+
39+
40+def docker_pull(container_name, registry=None, username=None, password=None):
41+ """
42+ Fetch a container from the Docker registry.
43+
44+ If :param:`registry` is given, the container will be pulled from that
45+ registry instead of the default. If the registry requires authentication,
46+ provide the :param:`username` and :param:`password` arguments.
47+ """
48+ if registry is not None:
49+ container_name = '/'.join([registry, container_name])
50+ if any(username, password):
51+ subprocess.check_call([
52+ 'docker', 'login', '-u', username, '-p', password])
53+ subprocess.check_call(['docker', 'pull', container_name])
54+
55+
56+class DockerCallback(ManagerCallback):
57+ """
58+ ServiceManager callback to manage starting up a Docker container.
59+
60+ Can be referenced as ``docker_start`` or ``docker_stop``, and performs
61+ the appropriate action. Requires one or more of
62+ :class:`DockerPortMappings`, :class:`DockerVolumes`,
63+ :class:`DockerContainerArgs`, and :class:`DockerRelation` to be
64+ included in the ``required_data`` section of the services definition.
65+
66+ Example::
67+
68+ manager = services.ServiceManager([{
69+ 'service': 'dockerfile/rethinkdb',
70+ 'required_data': [
71+ DockerPortMappings({
72+ 80: 8080,
73+ 28015: 28015,
74+ 29015: 29015,
75+ }),
76+ DockerVolumes(mapped_volumes={'data': '/rethinkdb'}),
77+ DockerContainerArgs(
78+ '--bind', 'all',
79+ '--canonical-address', hookenv.unit_get('public-address'),
80+ '--canonical-address', hookenv.unit_get('private-address'),
81+ '--machine-name', socket.gethostname().replace('-', '_'),
82+ ),
83+ ],
84+ 'start': docker_start,
85+ 'stop': docker_stop,
86+ }])
87+ """
88+ def __call__(self, manager, service_name, event_name):
89+ container_id_file = os.path.join(hookenv.charm_dir(), 'CONTAINER_ID')
90+ if os.path.exists(container_id_file):
91+ container_id = host.read_file(container_id_file)
92+ subprocess.check_call(['docker', 'stop', container_id])
93+ os.remove(container_id_file)
94+ if event_name == 'start':
95+ subprocess.check_call(
96+ ['docker', 'run', '-d', '--cidfile', container_id_file] +
97+ self.get_volume_args(manager, service_name) +
98+ self.get_port_args(manager, service_name) +
99+ [service_name] +
100+ self.get_container_args(manager, service_name))
101+
102+ def _get_args(self, manager, service_name, arg_type):
103+ args = []
104+ service = manager.get_service(service_name)
105+ for provider in service['required_data']:
106+ if isinstance(provider, arg_type):
107+ args.extend(provider.build_args())
108+ return args
109+
110+ def get_port_args(self, manager, service_name):
111+ return self._get_args(manager, service_name, DockerPortMappings)
112+
113+ def get_container_args(self, manager, service_name):
114+ return self._get_args(manager, service_name, DockerContainerArgs)
115+
116+ def get_volume_args(self, manager, service_name):
117+ return self._get_args(manager, service_name, DockerVolumes)
118+
119+
120+class DockerPortMappings(dict):
121+ """
122+ Context class for use in ``required_data`` representing a mapping of
123+ ports from the host to the container.
124+
125+ Example::
126+
127+ manager = services.ServiceManager([{
128+ 'required_data': [
129+ DockerPortMappings({
130+ 80: 8080,
131+ 28015: 28015,
132+ 29015: 29015,
133+ }),
134+ ],
135+ }])
136+
137+ """
138+ def build_args(self):
139+ ports = []
140+ for src, dst in self.iteritems():
141+ ports.extend(['-p', '{}:{}'.format(src, dst)])
142+ return ports
143+
144+
145+class DockerVolumes(object):
146+ """
147+ Context class for use in ``required_data`` representing Docker volumes.
148+
149+ Example::
150+
151+ manager = services.ServiceManager([{
152+ 'required_data': [
153+ DockerVolumes(mapped_volumes={'data': '/rethinkdb'}),
154+ ],
155+ 'start': docker_start,
156+ 'stop': docker_stop,
157+ }])
158+ """
159+ def __init__(self, volumes=None, named_volumes=None, mapped_volumes=None):
160+ """
161+ :param volumes: List of mutable data volumes to create.
162+ :type volumes: list
163+ :param named_volumes: Mapping of names to mutable data volumes to create.
164+ :type volumes: dict
165+ :param mapped_volumes: Mapping of host paths to container paths.
166+ :type volumes: dict
167+ """
168+ assert any([volumes, named_volumes, mapped_volumes]),\
169+ 'Must provide at least one of: volumes, named_volumes, mapped_volumes'
170+ self.volumes = volumes or []
171+ self.named_volumes = named_volumes or {}
172+ self.mapped_volumes = mapped_volumes or {}
173+
174+ def build_args(self):
175+ args = []
176+ for volume in self.volumes:
177+ args.extend(['-v', volume])
178+ for name, volume in self.named_volumes.iteritems():
179+ args.extend(['-v', volume, '--name', name])
180+ for host_path, volume in self.mapped_volumes.iteritems():
181+ if not os.path.isabs(host_path):
182+ host_path = os.path.join(hookenv.charm_dir(), host_path)
183+ host.mkdir(host_path)
184+ args.extend(['-v', ':'.join([host_path, volume])])
185+ return args
186+
187+
188+class DockerContainerArgs(object):
189+ """
190+ Context class for use in ``required_data`` representing arguments to be
191+ passed to the Docker container.
192+
193+ Example::
194+
195+ manager = services.ServiceManager([{
196+ 'required_data': [
197+ DockerContainerArgs(
198+ '--bind', 'all',
199+ '--canonical-address', hookenv.unit_get('public-address'),
200+ '--canonical-address', hookenv.unit_get('private-address'),
201+ '--machine-name', socket.gethostname().replace('-', '_'),
202+ ),
203+ ],
204+ 'start': docker_start,
205+ 'stop': docker_stop,
206+ }])
207+ """
208+ def __init__(self, *args, **kwargs):
209+ """
210+ Can accept either a list of arg strings, or a kwarg mapping of arg
211+ names to values. If kwargs are given, the names are prepended
212+ with '--' and have any underscores converted to dashes.
213+
214+ For example, `DockerContainerArgs(my_opt='foo')` becomes `['--my-opt', 'foo']`.
215+
216+ If you need to run a specific command other than the container default, it
217+ should be the first argument.
218+ """
219+ self.args = list(args)
220+ for key, value in kwargs.iteritems():
221+ self.args.extend(['--'+key.replace('_', '-'), value])
222+
223+ def build_args(self):
224+ return self.args
225+
226+
227+class DockerRelation(RelationContext, DockerContainerArgs):
228+ """
229+ :class:`~charmhelpers.core.services.helpers.RelationContext` subclass
230+ for use as a base class for contexts representing a relation to another
231+ Docker container, which could be another service, or a peer within the
232+ same service.
233+
234+ Example::
235+
236+ class DatabaseContainerRelation(DockerRelation):
237+ name = 'db'
238+ interface = 'mysql-docker'
239+ required_keys = ['db-host', 'db-name']
240+
241+ manager = services.ServiceManager([{
242+ 'required_data': [DatabaseContainerRelation()],
243+ 'start': docker_start,
244+ 'stop': docker_stop,
245+ }])
246+ """
247+ name = None
248+ interface = None
249+ required_keys = []
250+
251+ def map(self, relation_settings):
252+ """
253+ Translate the relation settings from a single unit into a list of
254+ arguments to be passed to the Docker container. This may be called
255+ multiple times, once for each unit, and the resulting arguments will
256+ all be passed to the container.
257+
258+ The default implementation simply appends ``--`` to the relation setting
259+ name, so that ``{'private-address': '10.0.0.1'}`` is transformed
260+ to ``['--private-address', '10.0.0.1']``.
261+
262+ For example, the following subclass would translate the ``private-address``
263+ value from each peer to a ``--join`` argument for each connected peer::
264+
265+ class ClusterPeers(DockerRelation):
266+ name = 'cluster'
267+ interface = 'cluster'
268+ required_keys = ['private-address']
269+ port = 29015
270+
271+ def map(self, relation_settings):
272+ return [
273+ '--join', '{}:{}'.format(
274+ relation_settings['private-address'],
275+ self.port
276+ )
277+ ]
278+ """
279+ args = []
280+ for key, value in relation_settings.iteritems():
281+ args.extend(['--{}'.format(key), str(value)])
282+ return args
283+
284+ def build_args(self):
285+ args = []
286+ for unit in self.get(self.name, []):
287+ args.extend(self.map(unit))
288+ return args
289+
290+
291+# Convenience aliases for Docker
292+docker_start = docker_stop = DockerCallback()
293
294=== modified file 'charmhelpers/core/host.py'
295--- charmhelpers/core/host.py 2014-08-05 20:52:59 +0000
296+++ charmhelpers/core/host.py 2014-08-18 16:28:55 +0000
297@@ -157,6 +157,11 @@
298 target.write(content)
299
300
301+def read_file(path):
302+ with open(path) as fp:
303+ return fp.read()
304+
305+
306 def fstab_remove(mp):
307 """Remove the given mountpoint entry from /etc/fstab
308 """
309
310=== modified file 'charmhelpers/core/services/base.py'
311--- charmhelpers/core/services/base.py 2014-08-05 21:28:01 +0000
312+++ charmhelpers/core/services/base.py 2014-08-18 16:28:55 +0000
313@@ -31,6 +31,7 @@
314 {
315 "service": <service name>,
316 "required_data": <list of required data contexts>,
317+ "provided_data": <list of provided data contexts>,
318 "data_ready": <one or more callbacks>,
319 "data_lost": <one or more callbacks>,
320 "start": <one or more callbacks>,
321@@ -44,6 +45,10 @@
322 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
323 information.
324
325+ The 'provided_data' list should contain relation data providers, most likely
326+ a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
327+ that will indicate a set of data to set on a given relation.
328+
329 The 'data_ready' value should be either a single callback, or a list of
330 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
331 Each callback will be called with the service name as the only parameter.
332@@ -123,12 +128,20 @@
333 self.reconfigure_services()
334
335 def provide_data(self):
336+ """
337+ Set the relation data for each provider in the ``provided_data`` list.
338+
339+ A provider must have a `name` attribute, which indicates which relation
340+ to set data on, and a `provide_data()` method, which returns a dict of
341+ data to set.
342+ """
343 hook_name = hookenv.hook_name()
344 for service in self.services.values():
345 for provider in service.get('provided_data', []):
346 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
347 data = provider.provide_data()
348- if provider._is_ready(data):
349+ _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
350+ if _ready:
351 hookenv.relation_set(None, data)
352
353 def reconfigure_services(self, *service_names):
354
355=== added file 'docs/api/charmhelpers.contrib.docker.rst'
356--- docs/api/charmhelpers.contrib.docker.rst 1970-01-01 00:00:00 +0000
357+++ docs/api/charmhelpers.contrib.docker.rst 2014-08-18 16:28:55 +0000
358@@ -0,0 +1,8 @@
359+charmhelpers.contrib.docker package
360+===================================
361+
362+.. automodule:: charmhelpers.contrib.docker
363+ :members:
364+ :undoc-members:
365+ :show-inheritance:
366+
367
368=== modified file 'docs/api/charmhelpers.contrib.rst'
369--- docs/api/charmhelpers.contrib.rst 2014-08-05 21:28:01 +0000
370+++ docs/api/charmhelpers.contrib.rst 2014-08-18 16:28:55 +0000
371@@ -6,6 +6,7 @@
372 charmhelpers.contrib.ansible
373 charmhelpers.contrib.charmhelpers
374 charmhelpers.contrib.charmsupport
375+ charmhelpers.contrib.docker
376 charmhelpers.contrib.hahelpers
377 charmhelpers.contrib.network
378 charmhelpers.contrib.openstack
379
380=== added file 'docs/examples/docker.rst'
381--- docs/examples/docker.rst 1970-01-01 00:00:00 +0000
382+++ docs/examples/docker.rst 2014-08-18 16:28:55 +0000
383@@ -0,0 +1,119 @@
384+Managing Docker with the Services Framework
385+===========================================
386+
387+Docker containers can be easily managed with
388+the :mod:`services framework <charmhelpers.core.services.base>` and the
389+helpers provided in :mod:`charmhelpers.contrib.docker`. Using these
390+helpers and classes will manage pulling the container, blocking on
391+neccessary data (such as from required config values, or other services),
392+starting the container with the appropriate arguments, and restarting the
393+container whenever any relevant configuration data has changed. And the
394+resulting charm will be very declarative and thus should be easy to follow.
395+
396+
397+Installing Docker and a Container
398+---------------------------------
399+
400+Your install hook will probably just consist of the following lines::
401+
402+ # hooks/install
403+
404+ from charmhelpers.contrib.docker import install_docker, docker_pull
405+
406+ install_docker()
407+ docker_pull('my_container')
408+
409+The :func:`~charmhelpers.contrib.docker.docker_pull` helper also has
410+support for pulling from a custom registry, with authentication.
411+
412+
413+Defining Your Docker Container Configuration
414+--------------------------------------------
415+
416+The common configuration for a Docker container includes port mappings,
417+volumes and / or volume mappings, and arguments for the container itself
418+which may be built from various sources such as config options, unit data,
419+or relations to other services. By using the various classes and
420+the :class:`docker_start() <charmhelpers.contrib.docker.DockerCallback>`
421+and :class:`docker_stop() <charmhelpers.contrib.docker.DockerCallback>`
422+helpers from :mod:`charmhelpers.contrib.docker` with the
423+:class:`~charmhelpers.core.services.base.ServiceManager`, you can declare
424+this configuration in a single location, and then just call
425+:meth:`manager.manage() <charmhelpers.core.services.base.ServiceManager.manage>`
426+to handle your hooks.
427+
428+An example, from the `RethinkDB Docker Charm <https://github.com/bcsaller/juju-docker/>`_,
429+which uses most of the options available, is below::
430+
431+ # hooks/hooks.py
432+ # (symlinked to from start, stop, config-changed, intracluster-relation-joined, etc)
433+
434+ import socket
435+ from charmhelpers.core import hookenv
436+ from charmhelpers.core import services
437+ from charmhelpers.contrib import docker
438+
439+ class ClusterPeers(docker.DockerRelation):
440+ name = 'intracluster'
441+ interface = 'rethinkdb-cluster'
442+ port = 29015
443+
444+ def map(self, relation_settings):
445+ return [
446+ '--join', '{}:{}'.format(
447+ relation_settings['private-address'],
448+ self.port
449+ )
450+ ]
451+
452+
453+ class WebsiteRelation(services.helpers.RelationContext):
454+ name = 'website'
455+ interface = 'http'
456+
457+ def provide_data(self):
458+ return {'hostname': hookenv.unit_private_ip(), 'port': 80}
459+
460+
461+ config = hookenv.config()
462+ manager = services.ServiceManager([{
463+ 'service': 'dockerfile/rethinkdb',
464+ 'ports': [80, 28015, 29015],
465+ 'provided_data': [WebsiteRelation()],
466+ 'required_data': [
467+ config if config.get('storage-path') else {},
468+ DockerPortMappings({
469+ 80: 8080,
470+ 28015: 28015,
471+ 29015: 29015,
472+ }),
473+ DockerVolumes(mapped_volumes={config.get('storage-path'): '/rethinkdb'}),
474+ DockerContainerArgs(
475+ bind='all',
476+ canonical_address=hookenv.unit_get('public-address'),
477+ canonical_address=hookenv.unit_get('private-address'),
478+ machine_name=socket.gethostname().replace('-', '_'),
479+ ),
480+ ClusterPeers(),
481+ ],
482+ 'start': docker_start,
483+ 'stop': docker_stop,
484+ }])
485+ manager.manage()
486+
487+In this example, once the ``storage-path`` config option has been set,
488+the container will be started with something like the following command-line::
489+
490+ docker run -d --cidfile CONTAINER_ID -v <storage-path>:/rethinkdb -p 80:8080 \
491+ -p 28015:28015 -p 29015:29015 dockerfile/rethinkdb --bind all \
492+ --canoncal-address <public-address> --canonical-address <private-address> \
493+ --machine-name <machine-name> --join <peer-addr>:<peer-port>
494+
495+Additionally, whenever a new peer is added via ``juju add-relation``, the container
496+will be automatically restarted with the additional ``--join`` argument.
497+
498+
499+Additional Steps
500+----------------
501+
502+Actually, that's it. =)
503
504=== added directory 'tests/contrib/docker'
505=== added file 'tests/contrib/docker/__init__.py'
506=== added file 'tests/contrib/docker/test_docker.py'
507--- tests/contrib/docker/test_docker.py 1970-01-01 00:00:00 +0000
508+++ tests/contrib/docker/test_docker.py 2014-08-18 16:28:55 +0000
509@@ -0,0 +1,118 @@
510+import mock
511+import unittest
512+
513+from charmhelpers.contrib import docker
514+
515+
516+class TestDockerHelpers(unittest.TestCase):
517+ def test_port_mappings(self):
518+ mappings = docker.DockerPortMappings({
519+ '80': '8080',
520+ '5000': '5000',
521+ })
522+ self.assertIn(mappings.build_args(), [
523+ ['-p', '80:8080', '-p', '5000:5000'],
524+ ['-p', '5000:5000', '-p', '80:8080'],
525+ ])
526+
527+ def test_container_args(self):
528+ args = docker.DockerContainerArgs(
529+ 'command',
530+ 'arg',
531+ named_arg='value',
532+ )
533+ self.assertEqual(args.build_args(), ['command', 'arg', '--named-arg', 'value'])
534+
535+ @mock.patch('charmhelpers.core.host.mkdir')
536+ @mock.patch('charmhelpers.core.hookenv.charm_dir')
537+ def test_volumes(self, charm_dir, mkdir):
538+ charm_dir.return_value = 'charm_dir'
539+ vols = docker.DockerVolumes(
540+ volumes=['vol1'],
541+ named_volumes={'vol2_name': 'vol2'},
542+ mapped_volumes={'host1': 'vol3'},
543+ )
544+ self.assertEqual(vols.build_args(), [
545+ '-v', 'vol1',
546+ '-v', 'vol2', '--name', 'vol2_name',
547+ '-v', 'charm_dir/host1:vol3'])
548+ mkdir.assert_called_once_with('charm_dir/host1')
549+ mkdir.reset_mock()
550+ vols = docker.DockerVolumes(mapped_volumes={'/host1': 'vol4'})
551+ self.assertEqual(vols.build_args(), ['-v', '/host1:vol4'])
552+ mkdir.assert_called_once_with('/host1')
553+
554+ @mock.patch('charmhelpers.core.host.mkdir')
555+ @mock.patch('os.path.exists')
556+ @mock.patch('os.remove')
557+ @mock.patch('subprocess.check_call')
558+ @mock.patch('charmhelpers.core.hookenv.charm_dir')
559+ @mock.patch('charmhelpers.core.host.read_file')
560+ def test_docker_callback(self, read_file, charm_dir, check_call, exists, remove, mkdir):
561+ charm_dir.return_value = 'charm_dir'
562+ read_file.return_value = 'cid'
563+ exists.return_value = True
564+ manager = mock.Mock()
565+ manager.get_service.return_value = {
566+ 'service': 'dockeruser/container',
567+ 'required_data': [
568+ docker.DockerPortMappings({'80': '8080'}),
569+ docker.DockerVolumes(mapped_volumes={'data': '/path'}),
570+ docker.DockerContainerArgs(bind='all'),
571+ ],
572+ }
573+ callback = docker.DockerCallback()
574+ callback(manager, 'dockeruser/container', 'start')
575+ exists.assert_called_once_with('charm_dir/CONTAINER_ID')
576+ read_file.assert_called_once_with('charm_dir/CONTAINER_ID')
577+ check_call.assert_has_call(['docker', 'stop', 'cid'])
578+ remove.assert_called_once_with('charm_dir/CONTAINER_ID')
579+ check_call.assert_has_call([
580+ 'docker', 'run', '-d', '-cidfile', 'charm_dir/CONTAINER_ID',
581+ '-v', 'charm_dir/data', '/path',
582+ '-p', '80:8080',
583+ 'dockeruser/container',
584+ '--bind', 'all',
585+ ])
586+
587+ @mock.patch('charmhelpers.core.services.helpers.RelationContext.get_data')
588+ def test_docker_relation(self, get_data):
589+ relation = docker.DockerRelation()
590+ relation.name = 'name'
591+ relation.required_keys = ['key1']
592+ relation.update({
593+ 'name': [
594+ {'key1': 'val1'},
595+ {'key2': 'val2'},
596+ ],
597+ })
598+ self.assertEqual(set(relation.build_args()), set([
599+ '--key1', 'val1', '--key2', 'val2',
600+ ]))
601+
602+ @mock.patch('charmhelpers.contrib.docker.open', create=True)
603+ @mock.patch('os.symlink')
604+ @mock.patch('os.unlink')
605+ @mock.patch('os.path.exists')
606+ @mock.patch('charmhelpers.fetch.apt_install')
607+ def test_install_docker(self, apt_install, exists, unlink, symlink, mopen):
608+ exists.return_value = True
609+ fp = mopen.return_value.__enter__.return_value
610+ docker.install_docker()
611+ assert apt_install.called
612+ assert exists.called
613+ assert unlink.called
614+ assert symlink.called
615+ assert fp.write.called
616+
617+ @mock.patch('charmhelpers.contrib.docker.fetch')
618+ def test_install_docker_unstable(self, fetch):
619+ docker.install_docker_unstable()
620+ assert fetch.add_source.called
621+ assert fetch.apt_update.called
622+ assert fetch.apt_install.called
623+
624+ @mock.patch('subprocess.check_call')
625+ def test_docker_pull(self, check_call):
626+ docker.docker_pull('foo')
627+ check_call.assert_called_once_with(['docker', 'pull', 'foo'])

Subscribers

People subscribed via source and target branches