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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/ansible.cfg +10 -1
- data/exe/.qb_interop_receive +3 -10
- data/exe/qb +8 -2
- data/lib/python/qb/__init__.py +6 -0
- data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
- data/lib/python/qb/ansible/modules/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/client.py +177 -0
- data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
- data/lib/python/qb/ipc/__init__.py +0 -0
- data/lib/python/qb/ipc/stdio/__init__.py +99 -0
- data/lib/python/qb/ipc/stdio/logging.py +151 -0
- data/lib/qb.rb +3 -3
- data/lib/qb/ansible/cmds/playbook.rb +5 -14
- data/lib/qb/ansible/env.rb +36 -6
- data/lib/qb/ansible/module.rb +396 -152
- data/lib/qb/ansible/module/response.rb +195 -0
- data/lib/qb/ansible/modules.rb +42 -0
- data/lib/qb/ansible/modules/docker/image.rb +273 -0
- data/lib/qb/cli.rb +5 -18
- data/lib/qb/cli/run.rb +2 -2
- data/lib/qb/data.rb +22 -0
- data/lib/qb/data/immutable.rb +39 -0
- data/lib/qb/docker.rb +2 -0
- data/lib/qb/docker/cli.rb +430 -0
- data/lib/qb/docker/image.rb +207 -0
- data/lib/qb/docker/image/name.rb +309 -0
- data/lib/qb/docker/image/tag.rb +113 -0
- data/lib/qb/docker/repo.rb +0 -0
- data/lib/qb/errors.rb +17 -3
- data/lib/qb/execution.rb +83 -0
- data/lib/qb/ipc.rb +48 -0
- data/lib/qb/ipc/stdio.rb +32 -0
- data/lib/qb/ipc/stdio/client.rb +267 -0
- data/lib/qb/ipc/stdio/server.rb +229 -0
- data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
- data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
- data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
- data/lib/qb/ipc/stdio/server/service.rb +229 -0
- data/lib/qb/options.rb +360 -502
- data/lib/qb/options/option.rb +293 -115
- data/lib/qb/options/option/option_parser_concern.rb +228 -0
- data/lib/qb/options/types.rb +73 -0
- data/lib/qb/package.rb +0 -1
- data/lib/qb/package/version.rb +179 -58
- data/lib/qb/package/version/from.rb +192 -51
- data/lib/qb/package/version/leveled.rb +1 -1
- data/lib/qb/path.rb +3 -2
- data/lib/qb/repo/git.rb +9 -85
- data/lib/qb/role/default_dir.rb +2 -2
- data/lib/qb/role/errors.rb +2 -8
- data/lib/qb/util.rb +1 -2
- data/lib/qb/util/bundler.rb +73 -43
- data/lib/qb/util/decorators.rb +99 -0
- data/lib/qb/util/interop.rb +7 -8
- data/lib/qb/util/resource.rb +12 -13
- data/lib/qb/version.rb +10 -0
- data/library/path_facts +5 -10
- data/library/qb.module.rb +105 -0
- data/library/stream +6 -26
- data/load/ansible/module/autorun.rb +25 -0
- data/load/ansible/module/script.rb +123 -0
- data/load/rebundle.rb +39 -0
- data/plugins/filter/dict_filters.py +56 -0
- data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
- data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
- data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
- data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
- data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
- data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
- data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
- data/plugins/test/dict_tests.py +36 -0
- data/plugins/test/string_tests.py +36 -0
- data/qb.gemspec +7 -3
- data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
- data/roles/nrser.state_mate/library/state +3 -17
- data/roles/qb/call/meta/qb.yml +1 -1
- data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
- data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
- data/roles/qb/git/check/clean/meta/qb.yml +1 -1
- data/roles/qb/git/ignore/meta/qb +10 -3
- data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
- data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
- data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
- data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
- data/roles/qb/osx/notif/meta/qb.yml +1 -1
- data/roles/qb/pkg/bump/library/bump +4 -16
- data/roles/qb/role/qb/defaults/main.yml +2 -0
- data/roles/qb/role/qb/meta/qb.yml +10 -5
- data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
- data/roles/qb/role/templates/library/module.rb.j2 +12 -23
- data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
- data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
- data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
- data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
- data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
- data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
- data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
- metadata +71 -22
- data/lib/qb/ansible_module.rb +0 -5
- data/lib/qb/util/stdio.rb +0 -187
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dd927b2c784f288a1bdadf4f4c4a6f06882bdc6
|
|
4
|
+
data.tar.gz: e182c25d31078f16d5942735f0f178b916e14067
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49174eac9d628957755b1eb620e03e49fd3b17eed4cb85727559c232948b80afea0ae4552551955da22ab4bf4cc46933272b5644cd65cd6170a400fa28e8f0f7
|
|
7
|
+
data.tar.gz: 4ea4f7d244e93d482de5f87d6da86b6658a087b9830b3101f2a9458c7909a83affa4546cf4314d0f6105591c9ec61e57d07f0f0e1fb0c87b9e5c8f7c3ba7cefc
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
data/ansible.cfg
CHANGED
|
@@ -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
|
data/exe/.qb_interop_receive
CHANGED
|
@@ -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
|
-
#
|
|
12
|
-
if 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::
|
|
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::
|
|
43
|
+
logger = NRSER::Log['qb/exe/qb#main']
|
|
41
44
|
|
|
42
45
|
QB::CLI.set_debug! args
|
|
43
|
-
NRSER::
|
|
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]
|
data/lib/python/qb/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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)
|