qb 0.3.25 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)