qb 0.3.25 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/ansible.cfg +10 -1
  4. data/exe/.qb_interop_receive +3 -10
  5. data/exe/qb +8 -2
  6. data/lib/python/qb/__init__.py +6 -0
  7. data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
  8. data/lib/python/qb/ansible/modules/__init__.py +0 -0
  9. data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
  10. data/lib/python/qb/ansible/modules/docker/client.py +177 -0
  11. data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
  12. data/lib/python/qb/ipc/__init__.py +0 -0
  13. data/lib/python/qb/ipc/stdio/__init__.py +99 -0
  14. data/lib/python/qb/ipc/stdio/logging.py +151 -0
  15. data/lib/qb.rb +3 -3
  16. data/lib/qb/ansible/cmds/playbook.rb +5 -14
  17. data/lib/qb/ansible/env.rb +36 -6
  18. data/lib/qb/ansible/module.rb +396 -152
  19. data/lib/qb/ansible/module/response.rb +195 -0
  20. data/lib/qb/ansible/modules.rb +42 -0
  21. data/lib/qb/ansible/modules/docker/image.rb +273 -0
  22. data/lib/qb/cli.rb +5 -18
  23. data/lib/qb/cli/run.rb +2 -2
  24. data/lib/qb/data.rb +22 -0
  25. data/lib/qb/data/immutable.rb +39 -0
  26. data/lib/qb/docker.rb +2 -0
  27. data/lib/qb/docker/cli.rb +430 -0
  28. data/lib/qb/docker/image.rb +207 -0
  29. data/lib/qb/docker/image/name.rb +309 -0
  30. data/lib/qb/docker/image/tag.rb +113 -0
  31. data/lib/qb/docker/repo.rb +0 -0
  32. data/lib/qb/errors.rb +17 -3
  33. data/lib/qb/execution.rb +83 -0
  34. data/lib/qb/ipc.rb +48 -0
  35. data/lib/qb/ipc/stdio.rb +32 -0
  36. data/lib/qb/ipc/stdio/client.rb +267 -0
  37. data/lib/qb/ipc/stdio/server.rb +229 -0
  38. data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
  39. data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
  40. data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
  41. data/lib/qb/ipc/stdio/server/service.rb +229 -0
  42. data/lib/qb/options.rb +360 -502
  43. data/lib/qb/options/option.rb +293 -115
  44. data/lib/qb/options/option/option_parser_concern.rb +228 -0
  45. data/lib/qb/options/types.rb +73 -0
  46. data/lib/qb/package.rb +0 -1
  47. data/lib/qb/package/version.rb +179 -58
  48. data/lib/qb/package/version/from.rb +192 -51
  49. data/lib/qb/package/version/leveled.rb +1 -1
  50. data/lib/qb/path.rb +3 -2
  51. data/lib/qb/repo/git.rb +9 -85
  52. data/lib/qb/role/default_dir.rb +2 -2
  53. data/lib/qb/role/errors.rb +2 -8
  54. data/lib/qb/util.rb +1 -2
  55. data/lib/qb/util/bundler.rb +73 -43
  56. data/lib/qb/util/decorators.rb +99 -0
  57. data/lib/qb/util/interop.rb +7 -8
  58. data/lib/qb/util/resource.rb +12 -13
  59. data/lib/qb/version.rb +10 -0
  60. data/library/path_facts +5 -10
  61. data/library/qb.module.rb +105 -0
  62. data/library/stream +6 -26
  63. data/load/ansible/module/autorun.rb +25 -0
  64. data/load/ansible/module/script.rb +123 -0
  65. data/load/rebundle.rb +39 -0
  66. data/plugins/filter/dict_filters.py +56 -0
  67. data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
  68. data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
  69. data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
  70. data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
  71. data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
  72. data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
  73. data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
  74. data/plugins/test/dict_tests.py +36 -0
  75. data/plugins/test/string_tests.py +36 -0
  76. data/qb.gemspec +7 -3
  77. data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
  78. data/roles/nrser.state_mate/library/state +3 -17
  79. data/roles/qb/call/meta/qb.yml +1 -1
  80. data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
  81. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
  82. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
  83. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
  84. data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
  85. data/roles/qb/git/check/clean/meta/qb.yml +1 -1
  86. data/roles/qb/git/ignore/meta/qb +10 -3
  87. data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
  88. data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
  89. data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
  90. data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
  91. data/roles/qb/osx/notif/meta/qb.yml +1 -1
  92. data/roles/qb/pkg/bump/library/bump +4 -16
  93. data/roles/qb/role/qb/defaults/main.yml +2 -0
  94. data/roles/qb/role/qb/meta/qb.yml +10 -5
  95. data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
  96. data/roles/qb/role/templates/library/module.rb.j2 +12 -23
  97. data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
  98. data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
  99. data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
  100. data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
  101. data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
  102. data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
  103. data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
  104. data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
  105. data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
  106. data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
  107. data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
  108. data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
  109. data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
  110. metadata +71 -22
  111. data/lib/qb/ansible_module.rb +0 -5
  112. data/lib/qb/util/stdio.rb +0 -187
  113. data/roles/qb/ruby/rspec/setup/tasks/main.yml +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e003d57b3df40cfe17e1d4200423a7dec88583cf
4
- data.tar.gz: 96bedb7ba58a2f2278f18dde6ead2c34533d105c
3
+ metadata.gz: 2dd927b2c784f288a1bdadf4f4c4a6f06882bdc6
4
+ data.tar.gz: e182c25d31078f16d5942735f0f178b916e14067
5
5
  SHA512:
6
- metadata.gz: d0211a56f9a20f60664bf169120b771283735b4eb9c68b6a2c26886b9d880cf0548720c3a09150748a1e06ec56e87ea926c44845d35d7207fe774df105e4fd99
7
- data.tar.gz: fc2fba112a5e43f3cdd41b7d8686d4a8e7d51070fd6a82838d6f4dae038594c6718a9f1bb63ecf3078bc82e2ef7959e96ecb1b494a5c6b6ed5afc6c78ae30e5f
6
+ metadata.gz: 49174eac9d628957755b1eb620e03e49fd3b17eed4cb85727559c232948b80afea0ae4552551955da22ab4bf4cc46933272b5644cd65cd6170a400fa28e8f0f7
7
+ data.tar.gz: 4ea4f7d244e93d482de5f87d6da86b6658a087b9830b3101f2a9458c7909a83affa4546cf4314d0f6105591c9ec61e57d07f0f0e1fb0c87b9e5c8f7c3ba7cefc
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.25
1
+ 0.4.0
@@ -1,6 +1,15 @@
1
+ ##
2
+ # This is the Ansible config used in local dev, when I'm running `qb` from
3
+ # the project root.
4
+ #
5
+ # It should probably be at `//dev/ansible.cfg` but I tried moving it there and
6
+ # it didn't work and I'm not gonna investigate now.
7
+ #
8
+ ##
9
+
1
10
  [defaults]
2
11
 
3
- roles_path = ./dev/scratch
12
+ roles_path = ./dev/scratch/roles
4
13
  retry_files_enabled = False
5
14
 
6
15
  # Will cause Ansible to cache facts at `//tmp/facts_cache/localhost` in JSON
@@ -8,15 +8,8 @@
8
8
  # not gonna right now. At least it's not a string in the Python file anymore.
9
9
  #
10
10
 
11
- # init bundler in dev env
12
- if ENV['QB_DEV_ENV']
13
- ENV.each {|k, v|
14
- if k.start_with? 'QB_DEV_ENV_'
15
- ENV[k.sub('QB_DEV_ENV_', '')] = v
16
- end
17
- }
18
- require 'bundler/setup'
19
- end
11
+ # Reinstate Bundler ENV vars if they have been moved
12
+ load ENV['QB_REBUNDLE_PATH'] if ENV['QB_REBUNDLE_PATH']
20
13
 
21
14
  # Set the thread name so that logs make sense.
22
15
  require 'thread'
@@ -29,7 +22,7 @@ require 'qb'
29
22
  if ENV['QB_STDIO_ERR']
30
23
  $stderr = UNIXSocket.new ENV['QB_STDIO_ERR']
31
24
 
32
- NRSER::Logging.setup_for_cli! application: 'qb'
25
+ NRSER::Log.setup_for_cli! application: 'qb'
33
26
 
34
27
  QB.debug "Connected to QB stderr stream at #{ ENV['QB_STDIO_ERR'] } #{ $stderr.path }."
35
28
  end
data/exe/qb CHANGED
@@ -26,6 +26,9 @@ require 'cmds'
26
26
  require 'qb'
27
27
 
28
28
 
29
+ # QB::IPC.is_master_process!
30
+
31
+
29
32
  # Refinements
30
33
  # =======================================================================
31
34
 
@@ -37,10 +40,10 @@ using NRSER
37
40
 
38
41
  def main *args
39
42
  Thread.current.name = 'main'
40
- logger = NRSER::Logging['qb/exe/qb#main']
43
+ logger = NRSER::Log['qb/exe/qb#main']
41
44
 
42
45
  QB::CLI.set_debug! args
43
- NRSER::Logging.setup_for_cli! application: 'qb'
46
+ NRSER::Log.setup_for_cli! application: 'qb'
44
47
 
45
48
  logger.debug args: args
46
49
 
@@ -59,6 +62,9 @@ def main *args
59
62
  [:setup, args.rest]
60
63
  when 'list', 'ls'
61
64
  [:list, *args.rest]
65
+ when 'root'
66
+ puts QB::ROOT.to_s
67
+ exit true
62
68
  else
63
69
  # default to `run` on the full args
64
70
  [:run, args]
@@ -0,0 +1,6 @@
1
+ import os
2
+
3
+ LIB_PYTHON_QB_DIR = os.path.dirname(os.path.realpath(__file__))
4
+
5
+ # Repo root directory, same as {QB::ROOT} in Ruby
6
+ ROOT = os.path.realpath(os.path.join(LIB_PYTHON_QB_DIR, '..', '..', '..'))
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Copyright 2016 Red Hat | Ansible
4
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
+
6
+ from __future__ import absolute_import, division, print_function
7
+ __metaclass__ = type
8
+
9
+ import json
10
+
11
+ from ansible.module_utils.docker_common import AnsibleDockerClient
12
+
13
+ import qb.ipc.stdio
14
+ import qb.ipc.stdio.logging
15
+
16
+
17
+ class QBAnsibleDockerClient(AnsibleDockerClient):
18
+
19
+ # Construction
20
+ # ========================================================================
21
+
22
+ def __init__(self, *args, **kwds):
23
+ self.logger = qb.ipc.stdio.logging.getLogger(
24
+ 'qb.ansible.modules.docker.client.QBAnsibleDockerClient'
25
+ )
26
+
27
+ AnsibleDockerClient.__init__(self, *args, **kwds)
28
+
29
+
30
+ # Instance Methods
31
+ # ============================================================================
32
+
33
+ # Logging and Output
34
+ # ----------------------------------------------------------------------------
35
+
36
+ def out(self, msg):
37
+ '''
38
+ Write output from the Docker daemon to STDOUT for the user to see
39
+ what's going on.
40
+
41
+ This used to be called `log` and was used for more than just logging
42
+ output from Docker, and it didn't do anything... though there was some
43
+ commented out code to write to a file.
44
+
45
+ Now it writes to the QB master process' STDOUT through
46
+ `QB::IPC::STDOUT`, assuming that's present.
47
+
48
+ :param msg - A string or dict.
49
+
50
+ :return: None
51
+ '''
52
+
53
+ # Bail unless QB STDOUT is connected
54
+ if not qb.ipc.stdio.client.stdout.connected:
55
+ return None
56
+
57
+ # What we're gonna write
58
+ string = None
59
+
60
+ if isinstance(msg, str):
61
+ # If the message is just a string, write that
62
+ string = msg
63
+
64
+ elif isinstance( msg, dict ):
65
+ # Dicts come for the Docker daemon/API, so we want to extract the
66
+ # relevant output and display that nicely... work in progress
67
+
68
+ if 'stream' in msg:
69
+ # This is part of an output 'stream' from a build,
70
+ # so just grab that that variable.
71
+ string = msg['stream']
72
+
73
+ elif 'status' in msg:
74
+ # This is part of an output from a pull (and maybe more?)
75
+
76
+ if 'id' in msg:
77
+ if 'progress' in msg:
78
+ string = "{id} {status} {progress}".format(**msg)
79
+ else:
80
+ string = "{id} {status}".format(**msg)
81
+ else:
82
+ string = msg['status']
83
+
84
+ else:
85
+ # Structures we're not sure how to deal with yet... just
86
+ # just pretty-dump them
87
+ string = json.dumps(
88
+ msg,
89
+ sort_keys=True,
90
+ indent=4,
91
+ separators=(',', ': ')
92
+ )
93
+ else:
94
+ self.logger.warning(
95
+ "Unregonized `msg` type {} in .out", type(msg),
96
+ payload=dict(msg=msg)
97
+ )
98
+
99
+ if string is not None:
100
+ qb.ipc.stdio.client.stdout.println(string)
101
+
102
+
103
+ def log(self, msg, pretty_print=False):
104
+ '''
105
+ Override :class:`AnsibleDockerClient.log` to actually do something.
106
+
107
+ It's used in :class:`AnsibleDockerClient` to do *both* logging and
108
+ relaying Docker API/daemon output, but we try to split it up and
109
+ send logging to :attr:`logger` and output to :meth:`out` - output seems
110
+ to always be `dict`, though this might be wrong.
111
+
112
+ :param msg: A stirng or dict.
113
+ :param pretty_print: Boolean, but not used - part of super API.
114
+
115
+ :return: None
116
+ '''
117
+
118
+ if isinstance(msg, dict):
119
+ self.out(msg)
120
+ elif isinstance(msg, str):
121
+ self.logger.info(msg)
122
+ else:
123
+ self.logger.warning(
124
+ "Unregonized `msg` type {} in .log".format(type(msg)),
125
+ payload=dict(msg=msg)
126
+ )
127
+
128
+
129
+ def fail(self, msg, **values):
130
+ '''
131
+ Overrides :class:`AnsibleDockerClient.fail` to log the failure first
132
+ (as `critical`/`fatal`).
133
+
134
+ Also adds feature to accept a dict of values which will be
135
+ :meth:`str.format` into the `msg` and also logged as the payload.
136
+
137
+ :param msg: String message, which may have `{key}` template markers
138
+ in it to be subsititued from `values`.
139
+ :param values: Optional dict of values to interpolate and log.
140
+
141
+ :return: See :class:`AnsibleDockerClient.fail`
142
+ '''
143
+
144
+ self.logger.critical(msg, payload=values)
145
+
146
+ return super(QBAnsibleDockerClient, self).fail(msg)
147
+
148
+
149
+ # Actions
150
+ # ------------------------------------------------------------------------
151
+
152
+ def try_pull_image(self, name, tag="latest"):
153
+ '''
154
+ Try to pull an image (before building or loading)
155
+ '''
156
+
157
+ self.logger.info(
158
+ "Attempting to pull image {}:{}".format(name, tag)
159
+ )
160
+
161
+ try:
162
+ for line in self.pull(name, tag=tag, stream=True, decode=True):
163
+ self.out(line)
164
+
165
+ if line.get('error'):
166
+ self.logger.info(
167
+ "Attempt to pull {}:{} failed".format(name, tag)
168
+ )
169
+ return None
170
+
171
+ except Exception as exc:
172
+ self.logger.warning(
173
+ "Error pulling image {}:{} - {}".format(name, tag, str(exc))
174
+ )
175
+ return None
176
+
177
+ return self.find_image(name=name, tag=tag)
@@ -0,0 +1,754 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Copyright 2016 Red Hat | Ansible
4
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
+
6
+
7
+ # Imports
8
+ # ============================================================================
9
+
10
+ from __future__ import absolute_import, division, print_function
11
+ __metaclass__ = type
12
+
13
+ import os
14
+ import re
15
+ import json
16
+ import logging
17
+
18
+ from ansible.module_utils.docker_common import (
19
+ HAS_DOCKER_PY_2,
20
+ AnsibleDockerClient,
21
+ DockerBaseClass
22
+ )
23
+
24
+ from ansible.module_utils._text import to_native
25
+
26
+ try:
27
+ if HAS_DOCKER_PY_2:
28
+ from docker.auth import resolve_repository_name
29
+ else:
30
+ from docker.auth.auth import resolve_repository_name
31
+ from docker.utils.utils import parse_repository_tag
32
+ except ImportError:
33
+ # missing docker-py handled in docker_common
34
+ pass
35
+
36
+
37
+ import qb.ipc.stdio
38
+ import qb.ipc.stdio.logging
39
+
40
+
41
+ # Globals
42
+ # ============================================================================
43
+
44
+ logger = qb.ipc.stdio.logging.getLogger('qb_docker_image')
45
+
46
+
47
+ # Casses
48
+ # ============================================================================
49
+
50
+ class ImageManager(DockerBaseClass):
51
+ '''
52
+ Adaptation and extension of the `ImageManager` class from Ansible's
53
+ `docker_image` module for QB's `qb_docker_image` module.
54
+ '''
55
+
56
+ # Construction
57
+ # ========================================================================
58
+
59
+ def __init__(self, client, results):
60
+
61
+ super(ImageManager, self).__init__()
62
+
63
+ self.client = client
64
+ self.results = results
65
+ parameters = self.client.module.params
66
+ self.check_mode = self.client.check_mode
67
+
68
+ self.archive_path = parameters.get('archive_path')
69
+ self.container_limits = parameters.get('container_limits')
70
+ self.dockerfile = parameters.get('dockerfile')
71
+ self.force = parameters.get('force')
72
+ self.load_path = parameters.get('load_path')
73
+ self.name = parameters.get('name')
74
+ self.nocache = parameters.get('nocache')
75
+ self.path = parameters.get('path')
76
+ self.pull = parameters.get('pull')
77
+ self.repository = parameters.get('repository')
78
+ self.rm = parameters.get('rm')
79
+ self.state = parameters.get('state')
80
+ self.tag = parameters.get('tag')
81
+ self.http_timeout = parameters.get('http_timeout')
82
+ self.push = parameters.get('push')
83
+ self.buildargs = parameters.get('buildargs')
84
+
85
+ # QB additions
86
+ self.try_to_pull = parameters.get('try_to_pull')
87
+
88
+ self.logger = qb.ipc.stdio.logging.getLogger(
89
+ 'qb_docker_image:ImageManager',
90
+ )
91
+
92
+ # If name contains a tag, it takes precedence over tag parameter.
93
+ repo, repo_tag = parse_repository_tag(self.name)
94
+ if repo_tag:
95
+ self.name = repo
96
+ self.tag = repo_tag
97
+
98
+ if self.state in ['present', 'build']:
99
+ self.present()
100
+ elif self.state == 'absent':
101
+ self.absent()
102
+
103
+ # END __init__
104
+
105
+
106
+ # Instance Methods
107
+ # ========================================================================
108
+
109
+ # Helpers
110
+ # ------------------------------------------------------------------------
111
+
112
+ def out(self, msg):
113
+ '''
114
+ Just proxies to :attr:`client.out`, which writes to the master QB
115
+ process' STDOUT (if available).
116
+
117
+ :param msg: String or dict with output, but should usually be a
118
+ dict with Docker API/daemon output in this case; log
119
+ messages using the :attr:`logger`.
120
+
121
+ :return: None
122
+ '''
123
+ self.client.out(msg)
124
+
125
+
126
+ def warn(self, warning, **values):
127
+ '''
128
+ Append a warning message to the `warnings` array of :attr:`results`
129
+ that will be returned to Ansible and log it as a warning.
130
+ '''
131
+ warning = str(warning)
132
+ self.results['warnings'].append(warning.format(**values))
133
+ self.logger.warning(warning, payload=values)
134
+
135
+
136
+ def fail(self, msg, **values):
137
+ '''
138
+ Proxy to :attr:`client.fail`.
139
+ '''
140
+ self.client.fail(msg, **values)
141
+
142
+
143
+ def append_action(self, msg, **values):
144
+ '''
145
+ Add a message to the `actions` array in :attr:`results` and log it
146
+ (as `info`).
147
+
148
+ I'm not sure what 'actions' are for, but they were here when I adapted
149
+ it... maybe something to do with "check mode"?
150
+ '''
151
+
152
+ formatted = msg.format(**values)
153
+ self.logger.info(formatted, payload=values)
154
+ self.results['actions'].append(formatted)
155
+
156
+
157
+ def image_summary(self, image):
158
+ return dict(
159
+ Id = image['Id'],
160
+ RepoTags = image['RepoTags'],
161
+ Created = image['Created'],
162
+ Metadata = image['Metadata'],
163
+ )
164
+
165
+
166
+ # States
167
+ # ------------------------------------------------------------------------
168
+
169
+ def present(self):
170
+ '''
171
+ Handles state = 'present', which includes building, loading or pulling
172
+ an image, depending on user provided parameters.
173
+
174
+ :return: None
175
+ '''
176
+
177
+ self.logger.debug(
178
+ "Starting state `present`...",
179
+ payload = dict(
180
+ name = self.name,
181
+ tag = self.tag,
182
+ )
183
+ )
184
+
185
+ existing_image = self.client.find_image(name=self.name, tag=self.tag)
186
+
187
+ if existing_image:
188
+ self.logger.info(
189
+ "Found existing image `{find_name}` in local daemon",
190
+ payload = dict(
191
+ find_name = "{}:{}".format(self.name, self.tag),
192
+ **self.image_summary(existing_image)
193
+ )
194
+ )
195
+
196
+ # Keep track of what images we get from where
197
+ pulled_image = None
198
+ built_image = None
199
+ loaded_image = None
200
+
201
+ if not existing_image or self.force:
202
+ # Try to pull if we're not forcing (which means we want to
203
+ # re-build/load regardless) and `self.try_to_pull` is `True`
204
+ if not self.force and self.try_to_pull:
205
+ self.append_action(
206
+ 'Tried to pull image `{name}:{tag}`',
207
+ name = self.name,
208
+ tag = self.tag
209
+ )
210
+
211
+ self.results['changed'] = True
212
+
213
+ if not self.check_mode:
214
+ pulled_image = self.client.try_pull_image(
215
+ self.name,
216
+ tag=self.tag
217
+ )
218
+
219
+ if pulled_image:
220
+ self.append_action(
221
+ 'Pulled image `{name}:{tag}`',
222
+ name = self.name,
223
+ tag = self.tag
224
+ )
225
+ self.results['image'] = pulled_image
226
+ # END if not self.force and self.try_to_pull:
227
+
228
+ if pulled_image is None:
229
+ if self.path:
230
+ # Build the image
231
+ if not os.path.isdir(self.path):
232
+ self.fail(
233
+ "Requested build path `{path}` could not be " +
234
+ "found or you do not have access.",
235
+ path = self.path,
236
+ )
237
+
238
+ image_name = self.name
239
+ if self.tag:
240
+ image_name = "%s:%s" % (self.name, self.tag)
241
+
242
+ self.logger.info(
243
+ "Building image `{image_name}`",
244
+ payload = dict(
245
+ image_name = image_name,
246
+ )
247
+ )
248
+
249
+ self.append_action(
250
+ "Built image `{image_name}` from `{path}`",
251
+ image_name = image_name,
252
+ path = self.path,
253
+ )
254
+
255
+ self.results['changed'] = True
256
+
257
+ if not self.check_mode:
258
+ built_image = self.build_image()
259
+ self.results['image'] = built_image
260
+
261
+ elif self.load_path:
262
+
263
+ # Load the image from an archive
264
+ if not os.path.isfile(self.load_path):
265
+ self.fail(
266
+ "Error loading image `{name}`. " +
267
+ "Specified load path `{load_path}` does not exist.",
268
+ name = self.name,
269
+ load_path = self.load_path,
270
+ )
271
+
272
+ image_name = self.name
273
+
274
+ if self.tag:
275
+ image_name = "%s:%s" % (self.name, self.tag)
276
+
277
+ self.append_action(
278
+ "Loaded image `{image_name}` from `{load_path}`",
279
+ image_name = image_name,
280
+ load_path = self.load_path,
281
+ )
282
+
283
+ self.results['changed'] = True
284
+
285
+ if not self.check_mode:
286
+ loaded_image = self.load_image()
287
+ self.results['image'] = loaded_image
288
+
289
+ else:
290
+ # pull the image
291
+ self.append_action(
292
+ 'Pulled image `{name}:{tag}`',
293
+ name = self.name,
294
+ tag = self.tag,
295
+ )
296
+
297
+ self.results['changed'] = True
298
+
299
+ if not self.check_mode:
300
+ pulled_image = self.client.pull_image(
301
+ self.name,
302
+ tag = self.tag,
303
+ )
304
+
305
+ self.results['image'] = pulled_image
306
+
307
+ if (
308
+ existing_image and
309
+ existing_image == self.results['image']
310
+ ):
311
+ self.results['changed'] = False
312
+ # END if pulled_image is None:
313
+ # END if not image or self.force:
314
+
315
+ # Archive the image if we have an archive path
316
+ if self.archive_path:
317
+ self.archive_image(self.name, self.tag)
318
+
319
+ # Tag the image to a repository if we have one
320
+ if self.repository:
321
+ self.tag_image(
322
+ self.name,
323
+ self.tag,
324
+ self.repository,
325
+ force = self.force,
326
+ push = self.push,
327
+ )
328
+
329
+ # This is weird to me logically, but I'm attempting to stick to the
330
+ # Ansible `docker_image` module behavior...
331
+ #
332
+ # We only push to the default repository (Docker Hub) if we didn't
333
+ # receive a `self.respository`. I guess this is for when you're using
334
+ # another repo than Docker Hub that you provide as `repository` and
335
+ # then it assumes you would never want to push to Docker Hub *too*.
336
+ #
337
+ # OK, that kinda makes sense to me...
338
+ #
339
+ elif self.push:
340
+ if pulled_image is not None:
341
+ # Regadless of anything, we never want to push an image we
342
+ # just pulled... makes no sense.
343
+ self.logger.debug(
344
+ "Image was pulled from repo, not pushing",
345
+ payload=self.image_summary(pulled_image)
346
+ )
347
+
348
+ else:
349
+ # Ok, now we can look at pushing...
350
+ if self.force:
351
+ # We're forcing, so force the push
352
+ self.logger.info(
353
+ "FORCING push of image `{name}:{tag}`...",
354
+ payload = dict(
355
+ name = self.name,
356
+ tag = self.tag,
357
+ )
358
+ )
359
+ self.push_image(self.name, self.tag)
360
+
361
+ else:
362
+ # We only want to push if we built or loaded an image
363
+ if built_image is not None:
364
+ self.logger.info(
365
+ "Pushing built image `{name}:{tag}`",
366
+ payload = dict(
367
+ name = self.name,
368
+ tag = self.tag,
369
+ **self.image_summary(built_image)
370
+ )
371
+ )
372
+ self.push_image(self.name, self.tag)
373
+
374
+ elif loaded_image is not None:
375
+ self.logger.info(
376
+ "Pushing loaded image `{name}:{tag}`",
377
+ payload = dict(
378
+ name = self.name,
379
+ tag = self.tag,
380
+ **self.image_summary(loaded_image)
381
+ )
382
+ )
383
+ self.push_image(self.name, self.tag)
384
+
385
+ else:
386
+ self.logger.info(
387
+ "No image built or loaded, not pushing"
388
+ )
389
+
390
+ # END if self.force / else
391
+ # END if pulled_image is not None / else
392
+ # END if self.repository / elif self.push
393
+
394
+ # QB addition - set the existing image as the result. Not sure why
395
+ # the Ansible version doesn't do this..?
396
+ if not self.results['image'] and existing_image is not None:
397
+ self.results['image'] = existing_image
398
+
399
+ self.logger.debug(
400
+ "State `present` done",
401
+ payload = dict(
402
+ results = self.results,
403
+ )
404
+ )
405
+ # END present()
406
+
407
+
408
+ def absent(self):
409
+ '''
410
+ Handles state = 'absent', which removes an image.
411
+
412
+ :return None
413
+ '''
414
+ image = self.client.find_image(self.name, self.tag)
415
+ if image:
416
+ name = self.name
417
+ if self.tag:
418
+ name = "%s:%s" % (self.name, self.tag)
419
+ if not self.check_mode:
420
+ try:
421
+ self.client.remove_image(name, force=self.force)
422
+ except Exception as exc:
423
+ self.fail("Error removing image %s - %s" % (name, str(exc)))
424
+
425
+ self.results['changed'] = True
426
+ self.append_action(
427
+ "Removed image `{name}`",
428
+ name = name,
429
+ )
430
+ self.results['image']['state'] = 'Deleted'
431
+
432
+
433
+ # Actions
434
+ # ------------------------------------------------------------------------
435
+
436
+ def archive_image(self, name, tag):
437
+ '''
438
+ Archive an image to a .tar file. Called when archive_path is passed.
439
+
440
+ :param name - name of the image. Type: str
441
+ :return None
442
+ '''
443
+
444
+ if not tag:
445
+ tag = "latest"
446
+
447
+ image = self.client.find_image(name=name, tag=tag)
448
+ if not image:
449
+ self.logger.info(
450
+ "archive image: image {name}:{tag} not found",
451
+ payload = dict(
452
+ name = name,
453
+ tag = tag,
454
+ )
455
+ )
456
+ return
457
+
458
+ image_name = "%s:%s" % (name, tag)
459
+
460
+ self.append_action(
461
+ 'Archived image `{image_name}` to `{archive_path}`',
462
+ image_name = image_name,
463
+ archive_path = self.archive_path
464
+ )
465
+
466
+ self.results['changed'] = True
467
+
468
+ if not self.check_mode:
469
+
470
+ self.logger.info(
471
+ "Getting archive of image `{image_name}`",
472
+ payload = dict(
473
+ image_name = image_name
474
+ )
475
+ )
476
+
477
+ try:
478
+ image = self.client.get_image(image_name)
479
+ except Exception as exc:
480
+ self.fail(
481
+ "Error getting image `%s` - %s" % (image_name, str(exc))
482
+ )
483
+
484
+ try:
485
+ with open(self.archive_path, 'w') as fd:
486
+ for chunk in image.stream(2048, decode_content=False):
487
+ fd.write(chunk)
488
+ except Exception as exc:
489
+ self.fail(
490
+ "Error writing image archive `%s` - %s" % (
491
+ self.archive_path,
492
+ str(exc)
493
+ )
494
+ )
495
+
496
+ image = self.client.find_image(name=name, tag=tag)
497
+ if image:
498
+ self.results['image'] = image
499
+
500
+
501
+ def push_image(self, name, tag=None):
502
+ '''
503
+ If the name of the image contains a repository path, then push the image.
504
+
505
+ :param name Name of the image to push.
506
+ :param tag Use a specific tag.
507
+ :return: None
508
+ '''
509
+
510
+ repository = name
511
+ if not tag:
512
+ repository, tag = parse_repository_tag(name)
513
+ registry, repo_name = resolve_repository_name(repository)
514
+
515
+ self.logger.info(
516
+ "push `{name}` to `{registry}/{repo_name}:{tag}`",
517
+ payload = dict(
518
+ name = self.name,
519
+ registry = registry,
520
+ repo_name = repo_name,
521
+ tag = tag
522
+ )
523
+ )
524
+
525
+ if registry:
526
+
527
+ self.append_action(
528
+ "Pushed image `{name}` to `{registry}/{repo_name}:{tag}`",
529
+ name = self.name,
530
+ registry = registry,
531
+ repo_name = repo_name,
532
+ tag = tag
533
+ )
534
+
535
+ self.results['changed'] = True
536
+
537
+ if not self.check_mode:
538
+ status = None
539
+ try:
540
+ for line in self.client.push(
541
+ repository,
542
+ tag = tag,
543
+ stream = True,
544
+ decode = True
545
+ ):
546
+ self.out(line)
547
+
548
+ if line.get('errorDetail'):
549
+ raise Exception(line['errorDetail']['message'])
550
+
551
+ status = line.get('status')
552
+
553
+ except Exception as exc:
554
+ if re.search('unauthorized', str(exc)):
555
+ if re.search('authentication required', str(exc)):
556
+ self.fail(
557
+ "Error pushing image %s/%s:%s - %s. Try logging into %s first." % (
558
+ registry,
559
+ repo_name,
560
+ tag,
561
+ str(exc),
562
+ registry
563
+ )
564
+ )
565
+ else:
566
+ self.fail(
567
+ "Error pushing image %s/%s:%s - %s. Does the repository exist?" % (
568
+ registry,
569
+ repo_name,
570
+ tag,
571
+ str(exc)
572
+ )
573
+ )
574
+
575
+ self.fail(
576
+ "Error pushing image %s: %s" % (repository, str(exc))
577
+ )
578
+
579
+ self.results['image'] = self.client.find_image(
580
+ name = repository,
581
+ tag = tag
582
+ )
583
+
584
+ if not self.results['image']:
585
+ self.results['image'] = dict()
586
+
587
+ self.results['image']['push_status'] = status
588
+
589
+
590
+ def tag_image(self, name, tag, repository, force=False, push=False):
591
+ '''
592
+ Tag an image into a repository.
593
+
594
+ :param name: name of the image. required.
595
+ :param tag: image tag.
596
+ :param repository: path to the repository. required.
597
+ :param force: bool. force tagging, even it image already exists with the repository path.
598
+ :param push: bool. push the image once it's tagged.
599
+ :return: None
600
+ '''
601
+ repo, repo_tag = parse_repository_tag(repository)
602
+
603
+ if not repo_tag:
604
+ repo_tag = "latest"
605
+ if tag:
606
+ repo_tag = tag
607
+
608
+ image = self.client.find_image(name=repo, tag=repo_tag)
609
+
610
+ found = 'found' if image else 'not found'
611
+
612
+ self.logger.info(
613
+ "image `{repo}` was `{found}`",
614
+ payload = dict(
615
+ repo = repo,
616
+ found = found
617
+ )
618
+ )
619
+
620
+ if not image or force:
621
+ self.logger.info(
622
+ "tagging {name}:{tag} to {repo}:{repo_tag}",
623
+ payload=dict(name=name, tag=tag, repo=repo, repo_tag=repo_tag)
624
+ )
625
+
626
+ self.results['changed'] = True
627
+
628
+ append_action(
629
+ "Tagged image {name}:{tag} to {repo}:{repo_tag}",
630
+ name=name, tag=tag, repo=repo, repo_tag=repo_tag
631
+ )
632
+
633
+ if not self.check_mode:
634
+ try:
635
+ # Finding the image does not always work, especially running a localhost registry. In those
636
+ # cases, if we don't set force=True, it errors.
637
+ image_name = name
638
+ if tag and not re.search(tag, name):
639
+ image_name = "%s:%s" % (name, tag)
640
+ tag_status = self.client.tag(image_name, repo, tag=repo_tag, force=True)
641
+ if not tag_status:
642
+ raise Exception("Tag operation failed.")
643
+ except Exception as exc:
644
+ self.fail("Error: failed to tag image - %s" % str(exc))
645
+ self.results['image'] = self.client.find_image(name=repo, tag=repo_tag)
646
+ if push:
647
+ self.push_image(repo, repo_tag)
648
+
649
+
650
+ def build_image(self):
651
+ '''
652
+ Build an image
653
+
654
+ :return: image dict
655
+ '''
656
+ params = dict(
657
+ path=self.path,
658
+ tag=self.name,
659
+ rm=self.rm,
660
+ nocache=self.nocache,
661
+ # Docker Pythong client v3 doesn't support
662
+ # stream=True,
663
+ timeout=self.http_timeout,
664
+ pull=self.pull,
665
+ forcerm=self.rm,
666
+ dockerfile=self.dockerfile,
667
+ decode=True
668
+ )
669
+ build_output = []
670
+ if self.tag:
671
+ params['tag'] = "%s:%s" % (self.name, self.tag)
672
+ if self.container_limits:
673
+ params['container_limits'] = self.container_limits
674
+ if self.buildargs:
675
+ for key, value in self.buildargs.items():
676
+ self.buildargs[key] = to_native(value)
677
+ params['buildargs'] = self.buildargs
678
+
679
+ self.logger.info(
680
+ "Building",
681
+ payload=params,
682
+ )
683
+
684
+ logs = self.client.build(**params)
685
+
686
+ # self.logger.info("build result", payload=dict(result=result))
687
+
688
+ for log in logs:
689
+
690
+ self.out(log)
691
+
692
+ if "stream" in log:
693
+ build_output.append(log["stream"])
694
+
695
+ if log.get('error'):
696
+ if log.get('errorDetail'):
697
+ errorDetail = log.get('errorDetail')
698
+ self.fail(
699
+ "Error building %s - code: %s, message: %s, logs: %s" % (
700
+ self.name,
701
+ errorDetail.get('code'),
702
+ errorDetail.get('message'),
703
+ build_output
704
+ )
705
+ )
706
+ else:
707
+ self.fail(
708
+ "Error building %s - message: %s, logs: %s" % (
709
+ self.name,
710
+ log.get('error'),
711
+ build_output
712
+ )
713
+ )
714
+ return self.client.find_image(name=self.name, tag=self.tag)
715
+
716
+
717
+ def load_image(self):
718
+ '''
719
+ Load an image from a .tar archive
720
+
721
+ :return: image dict
722
+ '''
723
+ try:
724
+ self.logger.info(
725
+ "Opening image `{load_path}`",
726
+ payload = dict(load_path=self.load_path)
727
+ )
728
+
729
+ image_tar = open(self.load_path, 'r')
730
+
731
+ except Exception as exc:
732
+ self.fail(
733
+ "Error opening image `{load_path}` - `{error}`",
734
+ load_path = self.load_path,
735
+ error = str(exc)
736
+ )
737
+
738
+ try:
739
+ self.logger.info(
740
+ "Loading image from `{load_path}`",
741
+ payload = dict(load_path=self.load_path)
742
+ )
743
+
744
+ self.client.load_image(image_tar)
745
+
746
+ except Exception as exc:
747
+ self.fail("Error loading image %s - %s" % (self.name, str(exc)))
748
+
749
+ try:
750
+ image_tar.close()
751
+ except Exception as exc:
752
+ self.fail("Error closing image %s - %s" % (self.name, str(exc)))
753
+
754
+ return self.client.find_image(self.name, self.tag)