1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Dario Zanzico (git@dariozanzico.com)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_stack
author: "Dario Zanzico (@dariko)"
short_description: docker stack module
description:
- Manage docker stacks using the C(docker stack) command
on the target node (see examples).
extends_documentation_fragment:
- community.docker.docker.cli_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: none
diff_mode:
support: none
action_group:
version_added: 3.6.0
options:
name:
description:
- Stack name
type: str
required: true
state:
description:
- Service state.
type: str
default: "present"
choices:
- present
- absent
compose:
description:
- List of compose definitions. Any element may be a string
referring to the path of the compose file on the target host
or the YAML contents of a compose file nested as dictionary.
type: list
elements: raw
default: []
prune:
description:
- If true will add the C(--prune) option to the C(docker stack deploy) command.
This will have docker remove the services not present in the
current stack definition.
type: bool
default: false
with_registry_auth:
description:
- If true will add the C(--with-registry-auth) option to the C(docker stack deploy) command.
This will have docker send registry authentication details to Swarm agents.
type: bool
default: false
resolve_image:
description:
- If set will add the C(--resolve-image) option to the C(docker stack deploy) command.
This will have docker query the registry to resolve image digest and
supported platforms. If not set, docker use "always" by default.
type: str
choices: ["always", "changed", "never"]
absent_retries:
description:
- If larger than V(0) and O(state=absent) the module will retry up to
O(absent_retries) times to delete the stack until all the
resources have been effectively deleted.
If the last try still reports the stack as not completely
removed the module will fail.
type: int
default: 0
absent_retries_interval:
description:
- Interval in seconds between consecutive O(absent_retries).
type: int
default: 1
docker_cli:
version_added: 3.6.0
docker_host:
version_added: 3.6.0
tls_hostname:
version_added: 3.6.0
api_version:
version_added: 3.6.0
ca_path:
version_added: 3.6.0
client_cert:
version_added: 3.6.0
client_key:
version_added: 3.6.0
tls:
version_added: 3.6.0
validate_certs:
version_added: 3.6.0
cli_context:
version_added: 3.6.0
requirements:
- Docker CLI tool C(docker)
- jsondiff
- pyyaml
'''
RETURN = '''
stack_spec_diff:
description: |
dictionary containing the differences between the 'Spec' field
of the stack services before and after applying the new stack
definition.
sample: >
"stack_spec_diff":
{'test_stack_test_service': {u'TaskTemplate': {u'ContainerSpec': {delete: [u'Env']}}}}
returned: on change
type: dict
'''
EXAMPLES = '''
- name: Deploy stack from a compose file
community.docker.docker_stack:
state: present
name: mystack
compose:
- /opt/docker-compose.yml
- name: Deploy stack from base compose file and override the web service
community.docker.docker_stack:
state: present
name: mystack
compose:
- /opt/docker-compose.yml
- version: '3'
services:
web:
image: nginx:latest
environment:
ENVVAR: envvar
- name: Remove stack
community.docker.docker_stack:
name: mystack
state: absent
'''
import json
import os
import tempfile
import traceback
from ansible.module_utils.six import string_types
from time import sleep
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
try:
from jsondiff import diff as json_diff
HAS_JSONDIFF = True
except ImportError:
HAS_JSONDIFF = False
try:
from yaml import dump as yaml_dump
HAS_YAML = True
except ImportError:
HAS_YAML = False
def docker_stack_services(client, stack_name):
rc, out, err = client.call_cli("stack", "services", stack_name, "--format", "{{.Name}}")
if to_native(err) == "Nothing found in stack: %s\n" % stack_name:
return []
return to_native(out).strip().split('\n')
def docker_service_inspect(client, service_name):
rc, out, err = client.call_cli("service", "inspect", service_name)
if rc != 0:
return None
else:
ret = json.loads(out)[0]['Spec']
return ret
def docker_stack_deploy(client, stack_name, compose_files):
command = ["stack", "deploy"]
if client.module.params["prune"]:
command += ["--prune"]
if client.module.params["with_registry_auth"]:
command += ["--with-registry-auth"]
if client.module.params["resolve_image"]:
command += ["--resolve-image",
client.module.params["resolve_image"]]
for compose_file in compose_files:
command += ["--compose-file",
compose_file]
command += [stack_name]
rc, out, err = client.call_cli(*command)
return rc, to_native(out), to_native(err)
def docker_stack_inspect(client, stack_name):
ret = {}
for service_name in docker_stack_services(client, stack_name):
ret[service_name] = docker_service_inspect(client, service_name)
return ret
def docker_stack_rm(client, stack_name, retries, interval):
command = ["stack", "rm", stack_name]
rc, out, err = client.call_cli(*command)
while to_native(err) != "Nothing found in stack: %s\n" % stack_name and retries > 0:
sleep(interval)
retries = retries - 1
rc, out, err = client.call_cli(*command)
return rc, to_native(out), to_native(err)
def main():
client = AnsibleModuleDockerClient(
argument_spec={
'name': dict(type='str', required=True),
'compose': dict(type='list', elements='raw', default=[]),
'prune': dict(type='bool', default=False),
'with_registry_auth': dict(type='bool', default=False),
'resolve_image': dict(type='str', choices=['always', 'changed', 'never']),
'state': dict(type='str', default='present', choices=['present', 'absent']),
'absent_retries': dict(type='int', default=0),
'absent_retries_interval': dict(type='int', default=1)
},
supports_check_mode=False,
)
if not HAS_JSONDIFF:
return client.fail("jsondiff is not installed, try 'pip install jsondiff'")
if not HAS_YAML:
return client.fail("yaml is not installed, try 'pip install pyyaml'")
try:
state = client.module.params['state']
compose = client.module.params['compose']
name = client.module.params['name']
absent_retries = client.module.params['absent_retries']
absent_retries_interval = client.module.params['absent_retries_interval']
if state == 'present':
if not compose:
client.fail("compose parameter must be a list containing at least one element")
compose_files = []
for i, compose_def in enumerate(compose):
if isinstance(compose_def, dict):
compose_file_fd, compose_file = tempfile.mkstemp()
client.module.add_cleanup_file(compose_file)
with os.fdopen(compose_file_fd, 'w') as stack_file:
compose_files.append(compose_file)
stack_file.write(yaml_dump(compose_def))
elif isinstance(compose_def, string_types):
compose_files.append(compose_def)
else:
client.fail("compose element '%s' must be a string or a dictionary" % compose_def)
before_stack_services = docker_stack_inspect(client, name)
rc, out, err = docker_stack_deploy(client, name, compose_files)
after_stack_services = docker_stack_inspect(client, name)
if rc != 0:
client.fail("docker stack up deploy command failed", rc=rc, stdout=out, stderr=err)
before_after_differences = json_diff(before_stack_services, after_stack_services)
for k in before_after_differences.keys():
if isinstance(before_after_differences[k], dict):
before_after_differences[k].pop('UpdatedAt', None)
before_after_differences[k].pop('Version', None)
if not list(before_after_differences[k].keys()):
before_after_differences.pop(k)
if not before_after_differences:
client.module.exit_json(
changed=False,
rc=rc,
stdout=out,
stderr=err,
)
else:
client.module.exit_json(
changed=True,
rc=rc,
stdout=out,
stderr=err,
stack_spec_diff=json_diff(
before_stack_services,
after_stack_services,
dump=True,
),
)
else:
if docker_stack_services(client, name):
rc, out, err = docker_stack_rm(client, name, absent_retries, absent_retries_interval)
if rc != 0:
client.module.fail_json(
msg="'docker stack down' command failed",
rc=rc,
stdout=out,
stderr=err,
)
else:
client.module.exit_json(
changed=True,
msg=out,
rc=rc,
stdout=out,
stderr=err,
)
client.module.exit_json(changed=False)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
if __name__ == "__main__":
main()
|