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
=== 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 @@
1
2import os
3import subprocess
4
5from charmhelpers import fetch
6from charmhelpers.core import host
7from charmhelpers.core import hookenv
8from charmhelpers.core.services.base import ManagerCallback
9from charmhelpers.core.services.helpers import RelationContext
10
11
12def install_docker():
13 """
14 Install the Docker tools.
15 """
16 fetch.apt_install(['docker.io'])
17 if os.path.exists('/usr/local/bin/docker'):
18 os.unlink('/usr/local/bin/docker')
19 os.symlink('/usr/bin/docker.io', '/usr/local/bin/docker')
20 with open('/etc/bash_completion.d/docker.io', 'a') as fp:
21 fp.write('\ncomplete -F _docker docker')
22
23
24def install_docker_unstable():
25 """
26 Install the Docker tools from the unstable (but potentially more
27 up-to-date) repository.
28 """
29 fetch.add_source('deb https://get.docker.io/ubuntu docker main',
30 key='36A1D7869245C8950F966E92D8576A8BA88D21E9')
31 fetch.apt_update(fatal=True)
32 fetch.apt_install(['lxc-docker'])
33
34
35def docker_pull(container_name, registry=None, username=None, password=None):
36 """
37 Fetch a container from the Docker registry.
38
39 If :param:`registry` is given, the container will be pulled from that
40 registry instead of the default. If the registry requires authentication,
41 provide the :param:`username` and :param:`password` arguments.
42 """
43 if registry is not None:
44 container_name = '/'.join([registry, container_name])
45 if any(username, password):
46 subprocess.check_call([
47 'docker', 'login', '-u', username, '-p', password])
48 subprocess.check_call(['docker', 'pull', container_name])
49
50
51class DockerCallback(ManagerCallback):
52 """
53 ServiceManager callback to manage starting up a Docker container.
54
55 Can be referenced as ``docker_start`` or ``docker_stop``, and performs
56 the appropriate action. Requires one or more of
57 :class:`DockerPortMappings`, :class:`DockerVolumes`,
58 :class:`DockerContainerArgs`, and :class:`DockerRelation` to be
59 included in the ``required_data`` section of the services definition.
60
61 Example::
62
63 manager = services.ServiceManager([{
64 'service': 'dockerfile/rethinkdb',
65 'required_data': [
66 DockerPortMappings({
67 80: 8080,
68 28015: 28015,
69 29015: 29015,
70 }),
71 DockerVolumes(mapped_volumes={'data': '/rethinkdb'}),
72 DockerContainerArgs(
73 '--bind', 'all',
74 '--canonical-address', hookenv.unit_get('public-address'),
75 '--canonical-address', hookenv.unit_get('private-address'),
76 '--machine-name', socket.gethostname().replace('-', '_'),
77 ),
78 ],
79 'start': docker_start,
80 'stop': docker_stop,
81 }])
82 """
83 def __call__(self, manager, service_name, event_name):
84 container_id_file = os.path.join(hookenv.charm_dir(), 'CONTAINER_ID')
85 if os.path.exists(container_id_file):
86 container_id = host.read_file(container_id_file)
87 subprocess.check_call(['docker', 'stop', container_id])
88 os.remove(container_id_file)
89 if event_name == 'start':
90 subprocess.check_call(
91 ['docker', 'run', '-d', '--cidfile', container_id_file] +
92 self.get_volume_args(manager, service_name) +
93 self.get_port_args(manager, service_name) +
94 [service_name] +
95 self.get_container_args(manager, service_name))
96
97 def _get_args(self, manager, service_name, arg_type):
98 args = []
99 service = manager.get_service(service_name)
100 for provider in service['required_data']:
101 if isinstance(provider, arg_type):
102 args.extend(provider.build_args())
103 return args
104
105 def get_port_args(self, manager, service_name):
106 return self._get_args(manager, service_name, DockerPortMappings)
107
108 def get_container_args(self, manager, service_name):
109 return self._get_args(manager, service_name, DockerContainerArgs)
110
111 def get_volume_args(self, manager, service_name):
112 return self._get_args(manager, service_name, DockerVolumes)
113
114
115class DockerPortMappings(dict):
116 """
117 Context class for use in ``required_data`` representing a mapping of
118 ports from the host to the container.
119
120 Example::
121
122 manager = services.ServiceManager([{
123 'required_data': [
124 DockerPortMappings({
125 80: 8080,
126 28015: 28015,
127 29015: 29015,
128 }),
129 ],
130 }])
131
132 """
133 def build_args(self):
134 ports = []
135 for src, dst in self.iteritems():
136 ports.extend(['-p', '{}:{}'.format(src, dst)])
137 return ports
138
139
140class DockerVolumes(object):
141 """
142 Context class for use in ``required_data`` representing Docker volumes.
143
144 Example::
145
146 manager = services.ServiceManager([{
147 'required_data': [
148 DockerVolumes(mapped_volumes={'data': '/rethinkdb'}),
149 ],
150 'start': docker_start,
151 'stop': docker_stop,
152 }])
153 """
154 def __init__(self, volumes=None, named_volumes=None, mapped_volumes=None):
155 """
156 :param volumes: List of mutable data volumes to create.
157 :type volumes: list
158 :param named_volumes: Mapping of names to mutable data volumes to create.
159 :type volumes: dict
160 :param mapped_volumes: Mapping of host paths to container paths.
161 :type volumes: dict
162 """
163 assert any([volumes, named_volumes, mapped_volumes]),\
164 'Must provide at least one of: volumes, named_volumes, mapped_volumes'
165 self.volumes = volumes or []
166 self.named_volumes = named_volumes or {}
167 self.mapped_volumes = mapped_volumes or {}
168
169 def build_args(self):
170 args = []
171 for volume in self.volumes:
172 args.extend(['-v', volume])
173 for name, volume in self.named_volumes.iteritems():
174 args.extend(['-v', volume, '--name', name])
175 for host_path, volume in self.mapped_volumes.iteritems():
176 if not os.path.isabs(host_path):
177 host_path = os.path.join(hookenv.charm_dir(), host_path)
178 host.mkdir(host_path)
179 args.extend(['-v', ':'.join([host_path, volume])])
180 return args
181
182
183class DockerContainerArgs(object):
184 """
185 Context class for use in ``required_data`` representing arguments to be
186 passed to the Docker container.
187
188 Example::
189
190 manager = services.ServiceManager([{
191 'required_data': [
192 DockerContainerArgs(
193 '--bind', 'all',
194 '--canonical-address', hookenv.unit_get('public-address'),
195 '--canonical-address', hookenv.unit_get('private-address'),
196 '--machine-name', socket.gethostname().replace('-', '_'),
197 ),
198 ],
199 'start': docker_start,
200 'stop': docker_stop,
201 }])
202 """
203 def __init__(self, *args, **kwargs):
204 """
205 Can accept either a list of arg strings, or a kwarg mapping of arg
206 names to values. If kwargs are given, the names are prepended
207 with '--' and have any underscores converted to dashes.
208
209 For example, `DockerContainerArgs(my_opt='foo')` becomes `['--my-opt', 'foo']`.
210
211 If you need to run a specific command other than the container default, it
212 should be the first argument.
213 """
214 self.args = list(args)
215 for key, value in kwargs.iteritems():
216 self.args.extend(['--'+key.replace('_', '-'), value])
217
218 def build_args(self):
219 return self.args
220
221
222class DockerRelation(RelationContext, DockerContainerArgs):
223 """
224 :class:`~charmhelpers.core.services.helpers.RelationContext` subclass
225 for use as a base class for contexts representing a relation to another
226 Docker container, which could be another service, or a peer within the
227 same service.
228
229 Example::
230
231 class DatabaseContainerRelation(DockerRelation):
232 name = 'db'
233 interface = 'mysql-docker'
234 required_keys = ['db-host', 'db-name']
235
236 manager = services.ServiceManager([{
237 'required_data': [DatabaseContainerRelation()],
238 'start': docker_start,
239 'stop': docker_stop,
240 }])
241 """
242 name = None
243 interface = None
244 required_keys = []
245
246 def map(self, relation_settings):
247 """
248 Translate the relation settings from a single unit into a list of
249 arguments to be passed to the Docker container. This may be called
250 multiple times, once for each unit, and the resulting arguments will
251 all be passed to the container.
252
253 The default implementation simply appends ``--`` to the relation setting
254 name, so that ``{'private-address': '10.0.0.1'}`` is transformed
255 to ``['--private-address', '10.0.0.1']``.
256
257 For example, the following subclass would translate the ``private-address``
258 value from each peer to a ``--join`` argument for each connected peer::
259
260 class ClusterPeers(DockerRelation):
261 name = 'cluster'
262 interface = 'cluster'
263 required_keys = ['private-address']
264 port = 29015
265
266 def map(self, relation_settings):
267 return [
268 '--join', '{}:{}'.format(
269 relation_settings['private-address'],
270 self.port
271 )
272 ]
273 """
274 args = []
275 for key, value in relation_settings.iteritems():
276 args.extend(['--{}'.format(key), str(value)])
277 return args
278
279 def build_args(self):
280 args = []
281 for unit in self.get(self.name, []):
282 args.extend(self.map(unit))
283 return args
284
285
286# Convenience aliases for Docker
287docker_start = docker_stop = DockerCallback()
0288
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2014-08-05 20:52:59 +0000
+++ charmhelpers/core/host.py 2014-08-18 16:28:55 +0000
@@ -157,6 +157,11 @@
157 target.write(content)157 target.write(content)
158158
159159
160def read_file(path):
161 with open(path) as fp:
162 return fp.read()
163
164
160def fstab_remove(mp):165def fstab_remove(mp):
161 """Remove the given mountpoint entry from /etc/fstab166 """Remove the given mountpoint entry from /etc/fstab
162 """167 """
163168
=== modified file 'charmhelpers/core/services/base.py'
--- charmhelpers/core/services/base.py 2014-08-05 21:28:01 +0000
+++ charmhelpers/core/services/base.py 2014-08-18 16:28:55 +0000
@@ -31,6 +31,7 @@
31 {31 {
32 "service": <service name>,32 "service": <service name>,
33 "required_data": <list of required data contexts>,33 "required_data": <list of required data contexts>,
34 "provided_data": <list of provided data contexts>,
34 "data_ready": <one or more callbacks>,35 "data_ready": <one or more callbacks>,
35 "data_lost": <one or more callbacks>,36 "data_lost": <one or more callbacks>,
36 "start": <one or more callbacks>,37 "start": <one or more callbacks>,
@@ -44,6 +45,10 @@
44 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more45 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
45 information.46 information.
4647
48 The 'provided_data' list should contain relation data providers, most likely
49 a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
50 that will indicate a set of data to set on a given relation.
51
47 The 'data_ready' value should be either a single callback, or a list of52 The 'data_ready' value should be either a single callback, or a list of
48 callbacks, to be called when all items in 'required_data' pass `is_ready()`.53 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49 Each callback will be called with the service name as the only parameter.54 Each callback will be called with the service name as the only parameter.
@@ -123,12 +128,20 @@
123 self.reconfigure_services()128 self.reconfigure_services()
124129
125 def provide_data(self):130 def provide_data(self):
131 """
132 Set the relation data for each provider in the ``provided_data`` list.
133
134 A provider must have a `name` attribute, which indicates which relation
135 to set data on, and a `provide_data()` method, which returns a dict of
136 data to set.
137 """
126 hook_name = hookenv.hook_name()138 hook_name = hookenv.hook_name()
127 for service in self.services.values():139 for service in self.services.values():
128 for provider in service.get('provided_data', []):140 for provider in service.get('provided_data', []):
129 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):141 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130 data = provider.provide_data()142 data = provider.provide_data()
131 if provider._is_ready(data):143 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
144 if _ready:
132 hookenv.relation_set(None, data)145 hookenv.relation_set(None, data)
133146
134 def reconfigure_services(self, *service_names):147 def reconfigure_services(self, *service_names):
135148
=== added file 'docs/api/charmhelpers.contrib.docker.rst'
--- docs/api/charmhelpers.contrib.docker.rst 1970-01-01 00:00:00 +0000
+++ docs/api/charmhelpers.contrib.docker.rst 2014-08-18 16:28:55 +0000
@@ -0,0 +1,8 @@
1charmhelpers.contrib.docker package
2===================================
3
4.. automodule:: charmhelpers.contrib.docker
5 :members:
6 :undoc-members:
7 :show-inheritance:
8
09
=== modified file 'docs/api/charmhelpers.contrib.rst'
--- docs/api/charmhelpers.contrib.rst 2014-08-05 21:28:01 +0000
+++ docs/api/charmhelpers.contrib.rst 2014-08-18 16:28:55 +0000
@@ -6,6 +6,7 @@
6 charmhelpers.contrib.ansible6 charmhelpers.contrib.ansible
7 charmhelpers.contrib.charmhelpers7 charmhelpers.contrib.charmhelpers
8 charmhelpers.contrib.charmsupport8 charmhelpers.contrib.charmsupport
9 charmhelpers.contrib.docker
9 charmhelpers.contrib.hahelpers10 charmhelpers.contrib.hahelpers
10 charmhelpers.contrib.network11 charmhelpers.contrib.network
11 charmhelpers.contrib.openstack12 charmhelpers.contrib.openstack
1213
=== added file 'docs/examples/docker.rst'
--- docs/examples/docker.rst 1970-01-01 00:00:00 +0000
+++ docs/examples/docker.rst 2014-08-18 16:28:55 +0000
@@ -0,0 +1,119 @@
1Managing Docker with the Services Framework
2===========================================
3
4Docker containers can be easily managed with
5the :mod:`services framework <charmhelpers.core.services.base>` and the
6helpers provided in :mod:`charmhelpers.contrib.docker`. Using these
7helpers and classes will manage pulling the container, blocking on
8neccessary data (such as from required config values, or other services),
9starting the container with the appropriate arguments, and restarting the
10container whenever any relevant configuration data has changed. And the
11resulting charm will be very declarative and thus should be easy to follow.
12
13
14Installing Docker and a Container
15---------------------------------
16
17Your install hook will probably just consist of the following lines::
18
19 # hooks/install
20
21 from charmhelpers.contrib.docker import install_docker, docker_pull
22
23 install_docker()
24 docker_pull('my_container')
25
26The :func:`~charmhelpers.contrib.docker.docker_pull` helper also has
27support for pulling from a custom registry, with authentication.
28
29
30Defining Your Docker Container Configuration
31--------------------------------------------
32
33The common configuration for a Docker container includes port mappings,
34volumes and / or volume mappings, and arguments for the container itself
35which may be built from various sources such as config options, unit data,
36or relations to other services. By using the various classes and
37the :class:`docker_start() <charmhelpers.contrib.docker.DockerCallback>`
38and :class:`docker_stop() <charmhelpers.contrib.docker.DockerCallback>`
39helpers from :mod:`charmhelpers.contrib.docker` with the
40:class:`~charmhelpers.core.services.base.ServiceManager`, you can declare
41this configuration in a single location, and then just call
42:meth:`manager.manage() <charmhelpers.core.services.base.ServiceManager.manage>`
43to handle your hooks.
44
45An example, from the `RethinkDB Docker Charm <https://github.com/bcsaller/juju-docker/>`_,
46which uses most of the options available, is below::
47
48 # hooks/hooks.py
49 # (symlinked to from start, stop, config-changed, intracluster-relation-joined, etc)
50
51 import socket
52 from charmhelpers.core import hookenv
53 from charmhelpers.core import services
54 from charmhelpers.contrib import docker
55
56 class ClusterPeers(docker.DockerRelation):
57 name = 'intracluster'
58 interface = 'rethinkdb-cluster'
59 port = 29015
60
61 def map(self, relation_settings):
62 return [
63 '--join', '{}:{}'.format(
64 relation_settings['private-address'],
65 self.port
66 )
67 ]
68
69
70 class WebsiteRelation(services.helpers.RelationContext):
71 name = 'website'
72 interface = 'http'
73
74 def provide_data(self):
75 return {'hostname': hookenv.unit_private_ip(), 'port': 80}
76
77
78 config = hookenv.config()
79 manager = services.ServiceManager([{
80 'service': 'dockerfile/rethinkdb',
81 'ports': [80, 28015, 29015],
82 'provided_data': [WebsiteRelation()],
83 'required_data': [
84 config if config.get('storage-path') else {},
85 DockerPortMappings({
86 80: 8080,
87 28015: 28015,
88 29015: 29015,
89 }),
90 DockerVolumes(mapped_volumes={config.get('storage-path'): '/rethinkdb'}),
91 DockerContainerArgs(
92 bind='all',
93 canonical_address=hookenv.unit_get('public-address'),
94 canonical_address=hookenv.unit_get('private-address'),
95 machine_name=socket.gethostname().replace('-', '_'),
96 ),
97 ClusterPeers(),
98 ],
99 'start': docker_start,
100 'stop': docker_stop,
101 }])
102 manager.manage()
103
104In this example, once the ``storage-path`` config option has been set,
105the container will be started with something like the following command-line::
106
107 docker run -d --cidfile CONTAINER_ID -v <storage-path>:/rethinkdb -p 80:8080 \
108 -p 28015:28015 -p 29015:29015 dockerfile/rethinkdb --bind all \
109 --canoncal-address <public-address> --canonical-address <private-address> \
110 --machine-name <machine-name> --join <peer-addr>:<peer-port>
111
112Additionally, whenever a new peer is added via ``juju add-relation``, the container
113will be automatically restarted with the additional ``--join`` argument.
114
115
116Additional Steps
117----------------
118
119Actually, that's it. =)
0120
=== added directory 'tests/contrib/docker'
=== added file 'tests/contrib/docker/__init__.py'
=== added file 'tests/contrib/docker/test_docker.py'
--- tests/contrib/docker/test_docker.py 1970-01-01 00:00:00 +0000
+++ tests/contrib/docker/test_docker.py 2014-08-18 16:28:55 +0000
@@ -0,0 +1,118 @@
1import mock
2import unittest
3
4from charmhelpers.contrib import docker
5
6
7class TestDockerHelpers(unittest.TestCase):
8 def test_port_mappings(self):
9 mappings = docker.DockerPortMappings({
10 '80': '8080',
11 '5000': '5000',
12 })
13 self.assertIn(mappings.build_args(), [
14 ['-p', '80:8080', '-p', '5000:5000'],
15 ['-p', '5000:5000', '-p', '80:8080'],
16 ])
17
18 def test_container_args(self):
19 args = docker.DockerContainerArgs(
20 'command',
21 'arg',
22 named_arg='value',
23 )
24 self.assertEqual(args.build_args(), ['command', 'arg', '--named-arg', 'value'])
25
26 @mock.patch('charmhelpers.core.host.mkdir')
27 @mock.patch('charmhelpers.core.hookenv.charm_dir')
28 def test_volumes(self, charm_dir, mkdir):
29 charm_dir.return_value = 'charm_dir'
30 vols = docker.DockerVolumes(
31 volumes=['vol1'],
32 named_volumes={'vol2_name': 'vol2'},
33 mapped_volumes={'host1': 'vol3'},
34 )
35 self.assertEqual(vols.build_args(), [
36 '-v', 'vol1',
37 '-v', 'vol2', '--name', 'vol2_name',
38 '-v', 'charm_dir/host1:vol3'])
39 mkdir.assert_called_once_with('charm_dir/host1')
40 mkdir.reset_mock()
41 vols = docker.DockerVolumes(mapped_volumes={'/host1': 'vol4'})
42 self.assertEqual(vols.build_args(), ['-v', '/host1:vol4'])
43 mkdir.assert_called_once_with('/host1')
44
45 @mock.patch('charmhelpers.core.host.mkdir')
46 @mock.patch('os.path.exists')
47 @mock.patch('os.remove')
48 @mock.patch('subprocess.check_call')
49 @mock.patch('charmhelpers.core.hookenv.charm_dir')
50 @mock.patch('charmhelpers.core.host.read_file')
51 def test_docker_callback(self, read_file, charm_dir, check_call, exists, remove, mkdir):
52 charm_dir.return_value = 'charm_dir'
53 read_file.return_value = 'cid'
54 exists.return_value = True
55 manager = mock.Mock()
56 manager.get_service.return_value = {
57 'service': 'dockeruser/container',
58 'required_data': [
59 docker.DockerPortMappings({'80': '8080'}),
60 docker.DockerVolumes(mapped_volumes={'data': '/path'}),
61 docker.DockerContainerArgs(bind='all'),
62 ],
63 }
64 callback = docker.DockerCallback()
65 callback(manager, 'dockeruser/container', 'start')
66 exists.assert_called_once_with('charm_dir/CONTAINER_ID')
67 read_file.assert_called_once_with('charm_dir/CONTAINER_ID')
68 check_call.assert_has_call(['docker', 'stop', 'cid'])
69 remove.assert_called_once_with('charm_dir/CONTAINER_ID')
70 check_call.assert_has_call([
71 'docker', 'run', '-d', '-cidfile', 'charm_dir/CONTAINER_ID',
72 '-v', 'charm_dir/data', '/path',
73 '-p', '80:8080',
74 'dockeruser/container',
75 '--bind', 'all',
76 ])
77
78 @mock.patch('charmhelpers.core.services.helpers.RelationContext.get_data')
79 def test_docker_relation(self, get_data):
80 relation = docker.DockerRelation()
81 relation.name = 'name'
82 relation.required_keys = ['key1']
83 relation.update({
84 'name': [
85 {'key1': 'val1'},
86 {'key2': 'val2'},
87 ],
88 })
89 self.assertEqual(set(relation.build_args()), set([
90 '--key1', 'val1', '--key2', 'val2',
91 ]))
92
93 @mock.patch('charmhelpers.contrib.docker.open', create=True)
94 @mock.patch('os.symlink')
95 @mock.patch('os.unlink')
96 @mock.patch('os.path.exists')
97 @mock.patch('charmhelpers.fetch.apt_install')
98 def test_install_docker(self, apt_install, exists, unlink, symlink, mopen):
99 exists.return_value = True
100 fp = mopen.return_value.__enter__.return_value
101 docker.install_docker()
102 assert apt_install.called
103 assert exists.called
104 assert unlink.called
105 assert symlink.called
106 assert fp.write.called
107
108 @mock.patch('charmhelpers.contrib.docker.fetch')
109 def test_install_docker_unstable(self, fetch):
110 docker.install_docker_unstable()
111 assert fetch.add_source.called
112 assert fetch.apt_update.called
113 assert fetch.apt_install.called
114
115 @mock.patch('subprocess.check_call')
116 def test_docker_pull(self, check_call):
117 docker.docker_pull('foo')
118 check_call.assert_called_once_with(['docker', 'pull', 'foo'])

Subscribers

People subscribed via source and target branches