Enable black formatting test (#259)

Enable black formatting test

SUMMARY
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
plugins/action/k8s_info.py
plugins/connection/kubectl.py
plugins/doc_fragments/helm_common_options.py
plugins/doc_fragments/k8s_auth_options.py
plugins/doc_fragments/k8s_delete_options.py
plugins/doc_fragments/k8s_name_options.py
plugins/doc_fragments/k8s_resource_options.py
plugins/doc_fragments/k8s_scale_options.py
plugins/doc_fragments/k8s_state_options.py
plugins/doc_fragments/k8s_wait_options.py
plugins/filter/k8s.py
plugins/inventory/k8s.py
plugins/lookup/k8s.py
plugins/lookup/kustomize.py
plugins/module_utils/ansiblemodule.py
plugins/module_utils/apply.py
plugins/module_utils/args_common.py
plugins/module_utils/client/discovery.py
plugins/module_utils/client/resource.py
plugins/module_utils/common.py
plugins/module_utils/exceptions.py
plugins/module_utils/hashes.py
plugins/module_utils/helm.py
plugins/module_utils/k8sdynamicclient.py
plugins/module_utils/selector.py
plugins/modules/helm.py
plugins/modules/helm_info.py
plugins/modules/helm_plugin.py
plugins/modules/helm_plugin_info.py
plugins/modules/helm_repository.py
plugins/modules/helm_template.py
plugins/modules/k8s.py
plugins/modules/k8s_cluster_info.py
plugins/modules/k8s_cp.py
plugins/modules/k8s_drain.py
plugins/modules/k8s_exec.py
plugins/modules/k8s_info.py
plugins/modules/k8s_json_patch.py
plugins/modules/k8s_log.py
plugins/modules/k8s_rollback.py
plugins/modules/k8s_scale.py
plugins/modules/k8s_service.py
tests/integration/targets/kubernetes/library/test_tempfile.py
tests/unit/module_utils/test_apply.py
tests/unit/module_utils/test_common.py
tests/unit/module_utils/test_discoverer.py
tests/unit/module_utils/test_hashes.py
tests/unit/module_utils/test_marshal.py
tests/unit/module_utils/test_selector.py
tox.ini

Reviewed-by: None <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
This commit is contained in:
Abhijeet Kasurde
2021-10-18 21:02:05 +05:30
committed by GitHub
parent 4010987d1f
commit 91b80b1d1d
50 changed files with 3453 additions and 2175 deletions

View File

@@ -3,7 +3,8 @@
# Copyright (c) 2020, Ansible Project # Copyright (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import copy import copy
@@ -13,7 +14,12 @@ from contextlib import contextmanager
from ansible.config.manager import ensure_type from ansible.config.manager import ensure_type
from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail from ansible.errors import (
AnsibleError,
AnsibleFileNotFound,
AnsibleAction,
AnsibleActionFail,
)
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import string_types, iteritems from ansible.module_utils.six import string_types, iteritems
from ansible.module_utils._text import to_text, to_bytes, to_native from ansible.module_utils._text import to_text, to_bytes, to_native
@@ -28,19 +34,19 @@ class ActionModule(ActionBase):
def _ensure_invocation(self, result): def _ensure_invocation(self, result):
# NOTE: adding invocation arguments here needs to be kept in sync with # NOTE: adding invocation arguments here needs to be kept in sync with
# any no_log specified in the argument_spec in the module. # any no_log specified in the argument_spec in the module.
if 'invocation' not in result: if "invocation" not in result:
if self._play_context.no_log: if self._play_context.no_log:
result['invocation'] = "CENSORED: no_log is set" result["invocation"] = "CENSORED: no_log is set"
else: else:
result['invocation'] = self._task.args.copy() result["invocation"] = self._task.args.copy()
result['invocation']['module_args'] = self._task.args.copy() result["invocation"]["module_args"] = self._task.args.copy()
return result return result
@contextmanager @contextmanager
def get_template_data(self, template_path): def get_template_data(self, template_path):
try: try:
source = self._find_needle('templates', template_path) source = self._find_needle("templates", template_path)
except AnsibleError as e: except AnsibleError as e:
raise AnsibleActionFail(to_text(e)) raise AnsibleActionFail(to_text(e))
@@ -48,15 +54,19 @@ class ActionModule(ActionBase):
try: try:
tmp_source = self._loader.get_real_file(source) tmp_source = self._loader.get_real_file(source)
except AnsibleFileNotFound as e: except AnsibleFileNotFound as e:
raise AnsibleActionFail("could not find template=%s, %s" % (source, to_text(e))) raise AnsibleActionFail(
b_tmp_source = to_bytes(tmp_source, errors='surrogate_or_strict') "could not find template=%s, %s" % (source, to_text(e))
)
b_tmp_source = to_bytes(tmp_source, errors="surrogate_or_strict")
try: try:
with open(b_tmp_source, 'rb') as f: with open(b_tmp_source, "rb") as f:
try: try:
template_data = to_text(f.read(), errors='surrogate_or_strict') template_data = to_text(f.read(), errors="surrogate_or_strict")
except UnicodeError: except UnicodeError:
raise AnsibleActionFail("Template source files must be utf-8 encoded") raise AnsibleActionFail(
"Template source files must be utf-8 encoded"
)
yield template_data yield template_data
except AnsibleAction: except AnsibleAction:
raise raise
@@ -73,62 +83,99 @@ class ActionModule(ActionBase):
"block_start_string": None, "block_start_string": None,
"block_end_string": None, "block_end_string": None,
"trim_blocks": True, "trim_blocks": True,
"lstrip_blocks": False "lstrip_blocks": False,
} }
if isinstance(template, string_types): if isinstance(template, string_types):
# treat this as raw_params # treat this as raw_params
template_param['path'] = template template_param["path"] = template
elif isinstance(template, dict): elif isinstance(template, dict):
template_args = template template_args = template
template_path = template_args.get('path', None) template_path = template_args.get("path", None)
if not template_path: if not template_path:
raise AnsibleActionFail("Please specify path for template.") raise AnsibleActionFail("Please specify path for template.")
template_param['path'] = template_path template_param["path"] = template_path
# Options type validation strings # Options type validation strings
for s_type in ('newline_sequence', 'variable_start_string', 'variable_end_string', 'block_start_string', for s_type in (
'block_end_string'): "newline_sequence",
"variable_start_string",
"variable_end_string",
"block_start_string",
"block_end_string",
):
if s_type in template_args: if s_type in template_args:
value = ensure_type(template_args[s_type], 'string') value = ensure_type(template_args[s_type], "string")
if value is not None and not isinstance(value, string_types): if value is not None and not isinstance(value, string_types):
raise AnsibleActionFail("%s is expected to be a string, but got %s instead" % (s_type, type(value))) raise AnsibleActionFail(
"%s is expected to be a string, but got %s instead"
% (s_type, type(value))
)
try: try:
template_param.update({ template_param.update(
"trim_blocks": boolean(template_args.get('trim_blocks', True), strict=False), {
"lstrip_blocks": boolean(template_args.get('lstrip_blocks', False), strict=False) "trim_blocks": boolean(
}) template_args.get("trim_blocks", True), strict=False
),
"lstrip_blocks": boolean(
template_args.get("lstrip_blocks", False), strict=False
),
}
)
except TypeError as e: except TypeError as e:
raise AnsibleActionFail(to_native(e)) raise AnsibleActionFail(to_native(e))
template_param.update({ template_param.update(
"newline_sequence": template_args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE), {
"variable_start_string": template_args.get('variable_start_string', None), "newline_sequence": template_args.get(
"variable_end_string": template_args.get('variable_end_string', None), "newline_sequence", self.DEFAULT_NEWLINE_SEQUENCE
"block_start_string": template_args.get('block_start_string', None), ),
"block_end_string": template_args.get('block_end_string', None) "variable_start_string": template_args.get(
}) "variable_start_string", None
),
"variable_end_string": template_args.get(
"variable_end_string", None
),
"block_start_string": template_args.get("block_start_string", None),
"block_end_string": template_args.get("block_end_string", None),
}
)
else: else:
raise AnsibleActionFail("Error while reading template file - " raise AnsibleActionFail(
"a string or dict for template expected, but got %s instead" % type(template)) "Error while reading template file - "
"a string or dict for template expected, but got %s instead"
% type(template)
)
return template_param return template_param
def import_jinja2_lstrip(self, templates): def import_jinja2_lstrip(self, templates):
# Option `lstrip_blocks' was added in Jinja2 version 2.7. # Option `lstrip_blocks' was added in Jinja2 version 2.7.
if any(tmp['lstrip_blocks'] for tmp in templates): if any(tmp["lstrip_blocks"] for tmp in templates):
try: try:
import jinja2.defaults import jinja2.defaults
except ImportError: except ImportError:
raise AnsibleError('Unable to import Jinja2 defaults for determining Jinja2 features.') raise AnsibleError(
"Unable to import Jinja2 defaults for determining Jinja2 features."
)
try: try:
jinja2.defaults.LSTRIP_BLOCKS jinja2.defaults.LSTRIP_BLOCKS
except AttributeError: except AttributeError:
raise AnsibleError("Option `lstrip_blocks' is only available in Jinja2 versions >=2.7") raise AnsibleError(
"Option `lstrip_blocks' is only available in Jinja2 versions >=2.7"
)
def load_template(self, template, new_module_args, task_vars): def load_template(self, template, new_module_args, task_vars):
# template is only supported by k8s module. # template is only supported by k8s module.
if self._task.action not in ('k8s', 'kubernetes.core.k8s', 'community.okd.k8s', 'redhat.openshift.k8s', 'community.kubernetes.k8s'): if self._task.action not in (
raise AnsibleActionFail("'template' is only a supported parameter for the 'k8s' module.") "k8s",
"kubernetes.core.k8s",
"community.okd.k8s",
"redhat.openshift.k8s",
"community.kubernetes.k8s",
):
raise AnsibleActionFail(
"'template' is only a supported parameter for the 'k8s' module."
)
template_params = [] template_params = []
if isinstance(template, string_types) or isinstance(template, dict): if isinstance(template, string_types) or isinstance(template, dict):
@@ -137,8 +184,11 @@ class ActionModule(ActionBase):
for element in template: for element in template:
template_params.append(self.get_template_args(element)) template_params.append(self.get_template_args(element))
else: else:
raise AnsibleActionFail("Error while reading template file - " raise AnsibleActionFail(
"a string or dict for template expected, but got %s instead" % type(template)) "Error while reading template file - "
"a string or dict for template expected, but got %s instead"
% type(template)
)
self.import_jinja2_lstrip(template_params) self.import_jinja2_lstrip(template_params)
@@ -149,20 +199,31 @@ class ActionModule(ActionBase):
old_vars = self._templar.available_variables old_vars = self._templar.available_variables
default_environment = {} default_environment = {}
for key in ("newline_sequence", "variable_start_string", "variable_end_string", for key in (
"block_start_string", "block_end_string", "trim_blocks", "lstrip_blocks"): "newline_sequence",
"variable_start_string",
"variable_end_string",
"block_start_string",
"block_end_string",
"trim_blocks",
"lstrip_blocks",
):
if hasattr(self._templar.environment, key): if hasattr(self._templar.environment, key):
default_environment[key] = getattr(self._templar.environment, key) default_environment[key] = getattr(self._templar.environment, key)
for template_item in template_params: for template_item in template_params:
# We need to convert unescaped sequences to proper escaped sequences for Jinja2 # We need to convert unescaped sequences to proper escaped sequences for Jinja2
newline_sequence = template_item['newline_sequence'] newline_sequence = template_item["newline_sequence"]
if newline_sequence in wrong_sequences: if newline_sequence in wrong_sequences:
template_item['newline_sequence'] = allowed_sequences[wrong_sequences.index(newline_sequence)] template_item["newline_sequence"] = allowed_sequences[
wrong_sequences.index(newline_sequence)
]
elif newline_sequence not in allowed_sequences: elif newline_sequence not in allowed_sequences:
raise AnsibleActionFail("newline_sequence needs to be one of: \n, \r or \r\n") raise AnsibleActionFail(
"newline_sequence needs to be one of: \n, \r or \r\n"
)
# template the source data locally & get ready to transfer # template the source data locally & get ready to transfer
with self.get_template_data(template_item['path']) as template_data: with self.get_template_data(template_item["path"]) as template_data:
# add ansible 'template' vars # add ansible 'template' vars
temp_vars = copy.deepcopy(task_vars) temp_vars = copy.deepcopy(task_vars)
for key, value in iteritems(template_item): for key, value in iteritems(template_item):
@@ -170,29 +231,45 @@ class ActionModule(ActionBase):
if value is not None: if value is not None:
setattr(self._templar.environment, key, value) setattr(self._templar.environment, key, value)
else: else:
setattr(self._templar.environment, key, default_environment.get(key)) setattr(
self._templar.environment,
key,
default_environment.get(key),
)
self._templar.available_variables = temp_vars self._templar.available_variables = temp_vars
result = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) result = self._templar.do_template(
template_data,
preserve_trailing_newlines=True,
escape_backslashes=False,
)
result_template.append(result) result_template.append(result)
self._templar.available_variables = old_vars self._templar.available_variables = old_vars
resource_definition = self._task.args.get('definition', None) resource_definition = self._task.args.get("definition", None)
if not resource_definition: if not resource_definition:
new_module_args.pop('template') new_module_args.pop("template")
new_module_args['definition'] = result_template new_module_args["definition"] = result_template
def get_file_realpath(self, local_path): def get_file_realpath(self, local_path):
# local_path is only supported by k8s_cp module. # local_path is only supported by k8s_cp module.
if self._task.action not in ('k8s_cp', 'kubernetes.core.k8s_cp', 'community.kubernetes.k8s_cp'): if self._task.action not in (
raise AnsibleActionFail("'local_path' is only supported parameter for 'k8s_cp' module.") "k8s_cp",
"kubernetes.core.k8s_cp",
"community.kubernetes.k8s_cp",
):
raise AnsibleActionFail(
"'local_path' is only supported parameter for 'k8s_cp' module."
)
if os.path.exists(local_path): if os.path.exists(local_path):
return local_path return local_path
try: try:
# find in expected paths # find in expected paths
return self._find_needle('files', local_path) return self._find_needle("files", local_path)
except AnsibleError: except AnsibleError:
raise AnsibleActionFail("%s does not exist in local filesystem" % local_path) raise AnsibleActionFail(
"%s does not exist in local filesystem" % local_path
)
def get_kubeconfig(self, kubeconfig, remote_transport, new_module_args): def get_kubeconfig(self, kubeconfig, remote_transport, new_module_args):
if isinstance(kubeconfig, string_types): if isinstance(kubeconfig, string_types):
@@ -200,20 +277,22 @@ class ActionModule(ActionBase):
if not remote_transport: if not remote_transport:
# kubeconfig is local # kubeconfig is local
# find in expected paths # find in expected paths
kubeconfig = self._find_needle('files', kubeconfig) kubeconfig = self._find_needle("files", kubeconfig)
# decrypt kubeconfig found # decrypt kubeconfig found
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True) actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
new_module_args['kubeconfig'] = actual_file new_module_args["kubeconfig"] = actual_file
elif isinstance(kubeconfig, dict): elif isinstance(kubeconfig, dict):
new_module_args['kubeconfig'] = kubeconfig new_module_args["kubeconfig"] = kubeconfig
else: else:
raise AnsibleActionFail("Error while reading kubeconfig parameter - " raise AnsibleActionFail(
"a string or dict expected, but got %s instead" % type(kubeconfig)) "Error while reading kubeconfig parameter - "
"a string or dict expected, but got %s instead" % type(kubeconfig)
)
def run(self, tmp=None, task_vars=None): def run(self, tmp=None, task_vars=None):
''' handler for k8s options ''' """ handler for k8s options """
if task_vars is None: if task_vars is None:
task_vars = dict() task_vars = dict()
@@ -224,53 +303,61 @@ class ActionModule(ActionBase):
# look for kubeconfig and src # look for kubeconfig and src
# 'local' => look files on Ansible Controller # 'local' => look files on Ansible Controller
# Transport other than 'local' => look files on remote node # Transport other than 'local' => look files on remote node
remote_transport = self._connection.transport != 'local' remote_transport = self._connection.transport != "local"
new_module_args = copy.deepcopy(self._task.args) new_module_args = copy.deepcopy(self._task.args)
kubeconfig = self._task.args.get('kubeconfig', None) kubeconfig = self._task.args.get("kubeconfig", None)
if kubeconfig: if kubeconfig:
try: try:
self.get_kubeconfig(kubeconfig, remote_transport, new_module_args) self.get_kubeconfig(kubeconfig, remote_transport, new_module_args)
except AnsibleError as e: except AnsibleError as e:
result['failed'] = True result["failed"] = True
result['msg'] = to_text(e) result["msg"] = to_text(e)
result['exception'] = traceback.format_exc() result["exception"] = traceback.format_exc()
return result return result
# find the file in the expected search path # find the file in the expected search path
src = self._task.args.get('src', None) src = self._task.args.get("src", None)
if src: if src:
if remote_transport: if remote_transport:
# src is on remote node # src is on remote node
result.update(self._execute_module(module_name=self._task.action, task_vars=task_vars)) result.update(
self._execute_module(
module_name=self._task.action, task_vars=task_vars
)
)
return self._ensure_invocation(result) return self._ensure_invocation(result)
# src is local # src is local
try: try:
# find in expected paths # find in expected paths
src = self._find_needle('files', src) src = self._find_needle("files", src)
except AnsibleError as e: except AnsibleError as e:
result['failed'] = True result["failed"] = True
result['msg'] = to_text(e) result["msg"] = to_text(e)
result['exception'] = traceback.format_exc() result["exception"] = traceback.format_exc()
return result return result
if src: if src:
new_module_args['src'] = src new_module_args["src"] = src
template = self._task.args.get('template', None) template = self._task.args.get("template", None)
if template: if template:
self.load_template(template, new_module_args, task_vars) self.load_template(template, new_module_args, task_vars)
local_path = self._task.args.get('local_path') local_path = self._task.args.get("local_path")
state = self._task.args.get('state', None) state = self._task.args.get("state", None)
if local_path and state == 'to_pod': if local_path and state == "to_pod":
new_module_args['local_path'] = self.get_file_realpath(local_path) new_module_args["local_path"] = self.get_file_realpath(local_path)
# Execute the k8s_* module. # Execute the k8s_* module.
module_return = self._execute_module(module_name=self._task.action, module_args=new_module_args, task_vars=task_vars) module_return = self._execute_module(
module_name=self._task.action,
module_args=new_module_args,
task_vars=task_vars,
)
# Delete tmp path # Delete tmp path
self._remove_tmp_path(self._connection._shell.tmpdir) self._remove_tmp_path(self._connection._shell.tmpdir)

View File

@@ -17,7 +17,8 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -185,26 +186,26 @@ from ansible.utils.display import Display
display = Display() display = Display()
CONNECTION_TRANSPORT = 'kubectl' CONNECTION_TRANSPORT = "kubectl"
CONNECTION_OPTIONS = { CONNECTION_OPTIONS = {
'kubectl_container': '-c', "kubectl_container": "-c",
'kubectl_namespace': '-n', "kubectl_namespace": "-n",
'kubectl_kubeconfig': '--kubeconfig', "kubectl_kubeconfig": "--kubeconfig",
'kubectl_context': '--context', "kubectl_context": "--context",
'kubectl_host': '--server', "kubectl_host": "--server",
'kubectl_username': '--username', "kubectl_username": "--username",
'kubectl_password': '--password', "kubectl_password": "--password",
'client_cert': '--client-certificate', "client_cert": "--client-certificate",
'client_key': '--client-key', "client_key": "--client-key",
'ca_cert': '--certificate-authority', "ca_cert": "--certificate-authority",
'validate_certs': '--insecure-skip-tls-verify', "validate_certs": "--insecure-skip-tls-verify",
'kubectl_token': '--token' "kubectl_token": "--token",
} }
class Connection(ConnectionBase): class Connection(ConnectionBase):
''' Local kubectl based connections ''' """ Local kubectl based connections """
transport = CONNECTION_TRANSPORT transport = CONNECTION_TRANSPORT
connection_options = CONNECTION_OPTIONS connection_options = CONNECTION_OPTIONS
@@ -217,57 +218,70 @@ class Connection(ConnectionBase):
# Note: kubectl runs commands as the user that started the container. # Note: kubectl runs commands as the user that started the container.
# It is impossible to set the remote user for a kubectl connection. # It is impossible to set the remote user for a kubectl connection.
cmd_arg = '{0}_command'.format(self.transport) cmd_arg = "{0}_command".format(self.transport)
if cmd_arg in kwargs: if cmd_arg in kwargs:
self.transport_cmd = kwargs[cmd_arg] self.transport_cmd = kwargs[cmd_arg]
else: else:
self.transport_cmd = distutils.spawn.find_executable(self.transport) self.transport_cmd = distutils.spawn.find_executable(self.transport)
if not self.transport_cmd: if not self.transport_cmd:
raise AnsibleError("{0} command not found in PATH".format(self.transport)) raise AnsibleError(
"{0} command not found in PATH".format(self.transport)
)
def _build_exec_cmd(self, cmd): def _build_exec_cmd(self, cmd):
""" Build the local kubectl exec command to run cmd on remote_host """Build the local kubectl exec command to run cmd on remote_host"""
"""
local_cmd = [self.transport_cmd] local_cmd = [self.transport_cmd]
censored_local_cmd = [self.transport_cmd] censored_local_cmd = [self.transport_cmd]
# Build command options based on doc string # Build command options based on doc string
doc_yaml = AnsibleLoader(self.documentation).get_single_data() doc_yaml = AnsibleLoader(self.documentation).get_single_data()
for key in doc_yaml.get('options'): for key in doc_yaml.get("options"):
if key.endswith('verify_ssl') and self.get_option(key) != '': if key.endswith("verify_ssl") and self.get_option(key) != "":
# Translate verify_ssl to skip_verify_ssl, and output as string # Translate verify_ssl to skip_verify_ssl, and output as string
skip_verify_ssl = not self.get_option(key) skip_verify_ssl = not self.get_option(key)
local_cmd.append(u'{0}={1}'.format(self.connection_options[key], str(skip_verify_ssl).lower())) local_cmd.append(
censored_local_cmd.append(u'{0}={1}'.format(self.connection_options[key], str(skip_verify_ssl).lower())) u"{0}={1}".format(
elif not key.endswith('container') and self.get_option(key) and self.connection_options.get(key): self.connection_options[key], str(skip_verify_ssl).lower()
)
)
censored_local_cmd.append(
u"{0}={1}".format(
self.connection_options[key], str(skip_verify_ssl).lower()
)
)
elif (
not key.endswith("container")
and self.get_option(key)
and self.connection_options.get(key)
):
cmd_arg = self.connection_options[key] cmd_arg = self.connection_options[key]
local_cmd += [cmd_arg, self.get_option(key)] local_cmd += [cmd_arg, self.get_option(key)]
# Redact password and token from console log # Redact password and token from console log
if key.endswith(('_token', '_password')): if key.endswith(("_token", "_password")):
censored_local_cmd += [cmd_arg, '********'] censored_local_cmd += [cmd_arg, "********"]
else: else:
censored_local_cmd += [cmd_arg, self.get_option(key)] censored_local_cmd += [cmd_arg, self.get_option(key)]
extra_args_name = u'{0}_extra_args'.format(self.transport) extra_args_name = u"{0}_extra_args".format(self.transport)
if self.get_option(extra_args_name): if self.get_option(extra_args_name):
local_cmd += self.get_option(extra_args_name).split(' ') local_cmd += self.get_option(extra_args_name).split(" ")
censored_local_cmd += self.get_option(extra_args_name).split(' ') censored_local_cmd += self.get_option(extra_args_name).split(" ")
pod = self.get_option(u'{0}_pod'.format(self.transport)) pod = self.get_option(u"{0}_pod".format(self.transport))
if not pod: if not pod:
pod = self._play_context.remote_addr pod = self._play_context.remote_addr
# -i is needed to keep stdin open which allows pipelining to work # -i is needed to keep stdin open which allows pipelining to work
local_cmd += ['exec', '-i', pod] local_cmd += ["exec", "-i", pod]
censored_local_cmd += ['exec', '-i', pod] censored_local_cmd += ["exec", "-i", pod]
# if the pod has more than one container, then container is required # if the pod has more than one container, then container is required
container_arg_name = u'{0}_container'.format(self.transport) container_arg_name = u"{0}_container".format(self.transport)
if self.get_option(container_arg_name): if self.get_option(container_arg_name):
local_cmd += ['-c', self.get_option(container_arg_name)] local_cmd += ["-c", self.get_option(container_arg_name)]
censored_local_cmd += ['-c', self.get_option(container_arg_name)] censored_local_cmd += ["-c", self.get_option(container_arg_name)]
local_cmd += ['--'] + cmd local_cmd += ["--"] + cmd
censored_local_cmd += ['--'] + cmd censored_local_cmd += ["--"] + cmd
return local_cmd, censored_local_cmd return local_cmd, censored_local_cmd
@@ -275,33 +289,45 @@ class Connection(ConnectionBase):
""" Connect to the container. Nothing to do """ """ Connect to the container. Nothing to do """
super(Connection, self)._connect() super(Connection, self)._connect()
if not self._connected: if not self._connected:
display.vvv(u"ESTABLISH {0} CONNECTION".format(self.transport), host=self._play_context.remote_addr) display.vvv(
u"ESTABLISH {0} CONNECTION".format(self.transport),
host=self._play_context.remote_addr,
)
self._connected = True self._connected = True
def exec_command(self, cmd, in_data=None, sudoable=False): def exec_command(self, cmd, in_data=None, sudoable=False):
""" Run a command in the container """ """ Run a command in the container """
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
local_cmd, censored_local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd]) local_cmd, censored_local_cmd = self._build_exec_cmd(
[self._play_context.executable, "-c", cmd]
)
display.vvv("EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr) display.vvv(
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd] "EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr
p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE, )
stdout=subprocess.PIPE, stderr=subprocess.PIPE) local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
p = subprocess.Popen(
local_cmd,
shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = p.communicate(in_data) stdout, stderr = p.communicate(in_data)
return (p.returncode, stdout, stderr) return (p.returncode, stdout, stderr)
def _prefix_login_path(self, remote_path): def _prefix_login_path(self, remote_path):
''' Make sure that we put files into a standard path """Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it. If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead. exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default. This also happens to be the former default.
Can revisit using $HOME instead if it's a problem Can revisit using $HOME instead if it's a problem
''' """
if not remote_path.startswith(os.path.sep): if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path) remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path) return os.path.normpath(remote_path)
@@ -309,61 +335,89 @@ class Connection(ConnectionBase):
def put_file(self, in_path, out_path): def put_file(self, in_path, out_path):
""" Transfer a file from local to the container """ """ Transfer a file from local to the container """
super(Connection, self).put_file(in_path, out_path) super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) display.vvv(
"PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
)
out_path = self._prefix_login_path(out_path) out_path = self._prefix_login_path(out_path)
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')): if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
raise AnsibleFileNotFound( raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
"file or module does not exist: %s" % in_path)
out_path = shlex_quote(out_path) out_path = shlex_quote(out_path)
# kubectl doesn't have native support for copying files into # kubectl doesn't have native support for copying files into
# running containers, so we use kubectl exec to implement this # running containers, so we use kubectl exec to implement this
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file: with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
if not os.fstat(in_file.fileno()).st_size: if not os.fstat(in_file.fileno()).st_size:
count = ' count=0' count = " count=0"
else: else:
count = '' count = ""
args, dummy = self._build_exec_cmd([self._play_context.executable, "-c", "dd of=%s bs=%s%s" % (out_path, BUFSIZE, count)]) args, dummy = self._build_exec_cmd(
args = [to_bytes(i, errors='surrogate_or_strict') for i in args] [
self._play_context.executable,
"-c",
"dd of=%s bs=%s%s" % (out_path, BUFSIZE, count),
]
)
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
try: try:
p = subprocess.Popen(args, stdin=in_file, p = subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.PIPE) args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError: except OSError:
raise AnsibleError("kubectl connection requires dd command in the container to put files") raise AnsibleError(
"kubectl connection requires dd command in the container to put files"
)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr)) raise AnsibleError(
"failed to transfer file %s to %s:\n%s\n%s"
% (in_path, out_path, stdout, stderr)
)
def fetch_file(self, in_path, out_path): def fetch_file(self, in_path, out_path):
""" Fetch a file from container to local. """ """ Fetch a file from container to local. """
super(Connection, self).fetch_file(in_path, out_path) super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) display.vvv(
"FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
)
in_path = self._prefix_login_path(in_path) in_path = self._prefix_login_path(in_path)
out_dir = os.path.dirname(out_path) out_dir = os.path.dirname(out_path)
# kubectl doesn't have native support for fetching files from # kubectl doesn't have native support for fetching files from
# running containers, so we use kubectl exec to implement this # running containers, so we use kubectl exec to implement this
args, dummy = self._build_exec_cmd([self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)]) args, dummy = self._build_exec_cmd(
args = [to_bytes(i, errors='surrogate_or_strict') for i in args] [self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)]
)
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
actual_out_path = os.path.join(out_dir, os.path.basename(in_path)) actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
with open(to_bytes(actual_out_path, errors='surrogate_or_strict'), 'wb') as out_file: with open(
to_bytes(actual_out_path, errors="surrogate_or_strict"), "wb"
) as out_file:
try: try:
p = subprocess.Popen(args, stdin=subprocess.PIPE, p = subprocess.Popen(
stdout=out_file, stderr=subprocess.PIPE) args, stdin=subprocess.PIPE, stdout=out_file, stderr=subprocess.PIPE
)
except OSError: except OSError:
raise AnsibleError( raise AnsibleError(
"{0} connection requires dd command in the container to fetch files".format(self.transport) "{0} connection requires dd command in the container to fetch files".format(
self.transport
)
) )
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
raise AnsibleError("failed to fetch file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr)) raise AnsibleError(
"failed to fetch file %s to %s:\n%s\n%s"
% (in_path, out_path, stdout, stderr)
)
if actual_out_path != out_path: if actual_out_path != out_path:
os.rename(to_bytes(actual_out_path, errors='strict'), to_bytes(out_path, errors='strict')) os.rename(
to_bytes(actual_out_path, errors="strict"),
to_bytes(out_path, errors="strict"),
)
def close(self): def close(self):
""" Terminate the connection. Nothing to do for kubectl""" """ Terminate the connection. Nothing to do for kubectl"""

View File

@@ -6,13 +6,14 @@
# Options for common Helm modules # Options for common Helm modules
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
binary_path: binary_path:
description: description:
@@ -56,4 +57,4 @@ options:
type: path type: path
aliases: [ ssl_ca_cert ] aliases: [ ssl_ca_cert ]
version_added: "1.2.0" version_added: "1.2.0"
''' """

View File

@@ -5,13 +5,14 @@
# Options for authenticating with the API. # Options for authenticating with the API.
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
host: host:
description: description:
@@ -114,4 +115,4 @@ notes:
- "To avoid SSL certificate validation errors when C(validate_certs) is I(True), the full - "To avoid SSL certificate validation errors when C(validate_certs) is I(True), the full
certificate chain for the API server must be provided via C(ca_cert) or in the certificate chain for the API server must be provided via C(ca_cert) or in the
kubeconfig file." kubeconfig file."
''' """

View File

@@ -5,13 +5,14 @@
# Options for specifying object wait # Options for specifying object wait
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
delete_options: delete_options:
type: dict type: dict
@@ -48,4 +49,4 @@ options:
type: str type: str
description: description:
- Specify the UID of the target object. - Specify the UID of the target object.
''' """

View File

@@ -5,13 +5,14 @@
# Options for selecting or identifying a specific K8s object # Options for selecting or identifying a specific K8s object
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
api_version: api_version:
description: description:
@@ -49,4 +50,4 @@ options:
- If I(resource definition) is provided, the I(metadata.namespace) value from the I(resource_definition) - If I(resource definition) is provided, the I(metadata.namespace) value from the I(resource_definition)
will override this option. will override this option.
type: str type: str
''' """

View File

@@ -5,13 +5,14 @@
# Options for providing an object configuration # Options for providing an object configuration
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
resource_definition: resource_definition:
description: description:
@@ -30,4 +31,4 @@ options:
I(resource_definition). See Examples below. I(resource_definition). See Examples below.
- Mutually exclusive with I(template) in case of M(k8s) module. - Mutually exclusive with I(template) in case of M(k8s) module.
type: path type: path
''' """

View File

@@ -5,13 +5,14 @@
# Options used by scale modules. # Options used by scale modules.
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
replicas: replicas:
description: description:
@@ -46,4 +47,4 @@ options:
default: 5 default: 5
type: int type: int
version_added: 2.0.0 version_added: 2.0.0
''' """

View File

@@ -5,13 +5,14 @@
# Options for specifying object state # Options for specifying object state
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
state: state:
description: description:
@@ -27,4 +28,4 @@ options:
- If set to C(yes), and I(state) is C(present), an existing object will be replaced. - If set to C(yes), and I(state) is C(present), an existing object will be replaced.
type: bool type: bool
default: no default: no
''' """

View File

@@ -5,13 +5,14 @@
# Options for specifying object wait # Options for specifying object wait
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
class ModuleDocFragment(object): class ModuleDocFragment(object):
DOCUMENTATION = r''' DOCUMENTATION = r"""
options: options:
wait: wait:
description: description:
@@ -64,4 +65,4 @@ options:
- The possible reasons in a condition are specific to each resource type in Kubernetes. - The possible reasons in a condition are specific to each resource type in Kubernetes.
- See the API documentation of the status field for a given resource to see possible choices. - See the API documentation of the status field for a given resource to see possible choices.
type: dict type: dict
''' """

View File

@@ -2,12 +2,15 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
generate_hash,
)
def k8s_config_resource_name(resource): def k8s_config_resource_name(resource):
@@ -15,15 +18,14 @@ def k8s_config_resource_name(resource):
Generate resource name for the given resource of type ConfigMap, Secret Generate resource name for the given resource of type ConfigMap, Secret
""" """
try: try:
return resource['metadata']['name'] + '-' + generate_hash(resource) return resource["metadata"]["name"] + "-" + generate_hash(resource)
except KeyError: except KeyError:
raise AnsibleFilterError("resource must have a metadata.name key to generate a resource name") raise AnsibleFilterError(
"resource must have a metadata.name key to generate a resource name"
)
# ---- Ansible filters ---- # ---- Ansible filters ----
class FilterModule(object): class FilterModule(object):
def filters(self): def filters(self):
return { return {"k8s_config_resource_name": k8s_config_resource_name}
'k8s_config_resource_name': k8s_config_resource_name
}

View File

@@ -1,10 +1,11 @@
# Copyright (c) 2018 Ansible Project # Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = """
name: k8s name: k8s
plugin_type: inventory plugin_type: inventory
author: author:
@@ -89,9 +90,9 @@ DOCUMENTATION = '''
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = ''' EXAMPLES = """
# File must be named k8s.yaml or k8s.yml # File must be named k8s.yaml or k8s.yml
# Authenticate with token, and return all pods and services for all namespaces # Authenticate with token, and return all pods and services for all namespaces
@@ -112,12 +113,17 @@ plugin: kubernetes.core.k8s
connections: connections:
- kubeconfig: /path/to/config - kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer' context: 'awx/192-168-64-4:8443/developer'
''' """
import json import json
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception, get_api_client from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
HAS_K8S_MODULE_HELPER,
k8s_import_exception,
get_api_client,
)
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
try: try:
@@ -128,13 +134,13 @@ except ImportError:
def format_dynamic_api_exc(exc): def format_dynamic_api_exc(exc):
if exc.body: if exc.body:
if exc.headers and exc.headers.get('Content-Type') == 'application/json': if exc.headers and exc.headers.get("Content-Type") == "application/json":
message = json.loads(exc.body).get('message') message = json.loads(exc.body).get("message")
if message: if message:
return message return message
return exc.body return exc.body
else: else:
return '%s Reason: %s' % (exc.status, exc.reason) return "%s Reason: %s" % (exc.status, exc.reason)
class K8sInventoryException(Exception): class K8sInventoryException(Exception):
@@ -142,10 +148,10 @@ class K8sInventoryException(Exception):
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin): class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin):
NAME = 'kubernetes.core.k8s' NAME = "kubernetes.core.k8s"
connection_plugin = 'kubernetes.core.kubectl' connection_plugin = "kubernetes.core.kubectl"
transport = 'kubectl' transport = "kubectl"
def parse(self, inventory, loader, path, cache=True): def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path) super(InventoryModule, self).parse(inventory, loader, path)
@@ -154,11 +160,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
self.setup(config_data, cache, cache_key) self.setup(config_data, cache, cache_key)
def setup(self, config_data, cache, cache_key): def setup(self, config_data, cache, cache_key):
connections = config_data.get('connections') connections = config_data.get("connections")
if not HAS_K8S_MODULE_HELPER: if not HAS_K8S_MODULE_HELPER:
raise K8sInventoryException( raise K8sInventoryException(
"This module requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(k8s_import_exception) "This module requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(
k8s_import_exception
)
) )
source_data = None source_data = None
@@ -179,11 +187,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
for connection in connections: for connection in connections:
if not isinstance(connection, dict): if not isinstance(connection, dict):
raise K8sInventoryException("Expecting connection to be a dictionary.") raise K8sInventoryException(
"Expecting connection to be a dictionary."
)
client = get_api_client(**connection) client = get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host)) name = connection.get(
if connection.get('namespaces'): "name", self.get_default_host_name(client.configuration.host)
namespaces = connection['namespaces'] )
if connection.get("namespaces"):
namespaces = connection["namespaces"]
else: else:
namespaces = self.get_available_namespaces(client) namespaces = self.get_available_namespaces(client)
for namespace in namespaces: for namespace in namespaces:
@@ -199,27 +211,36 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
@staticmethod @staticmethod
def get_default_host_name(host): def get_default_host_name(host):
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_') return (
host.replace("https://", "")
.replace("http://", "")
.replace(".", "-")
.replace(":", "_")
)
def get_available_namespaces(self, client): def get_available_namespaces(self, client):
v1_namespace = client.resources.get(api_version='v1', kind='Namespace') v1_namespace = client.resources.get(api_version="v1", kind="Namespace")
try: try:
obj = v1_namespace.get() obj = v1_namespace.get()
except DynamicApiError as exc: except DynamicApiError as exc:
self.display.debug(exc) self.display.debug(exc)
raise K8sInventoryException('Error fetching Namespace list: %s' % format_dynamic_api_exc(exc)) raise K8sInventoryException(
"Error fetching Namespace list: %s" % format_dynamic_api_exc(exc)
)
return [namespace.metadata.name for namespace in obj.items] return [namespace.metadata.name for namespace in obj.items]
def get_pods_for_namespace(self, client, name, namespace): def get_pods_for_namespace(self, client, name, namespace):
v1_pod = client.resources.get(api_version='v1', kind='Pod') v1_pod = client.resources.get(api_version="v1", kind="Pod")
try: try:
obj = v1_pod.get(namespace=namespace) obj = v1_pod.get(namespace=namespace)
except DynamicApiError as exc: except DynamicApiError as exc:
self.display.debug(exc) self.display.debug(exc)
raise K8sInventoryException('Error fetching Pod list: %s' % format_dynamic_api_exc(exc)) raise K8sInventoryException(
"Error fetching Pod list: %s" % format_dynamic_api_exc(exc)
)
namespace_group = 'namespace_{0}'.format(namespace) namespace_group = "namespace_{0}".format(namespace)
namespace_pods_group = '{0}_pods'.format(namespace_group) namespace_pods_group = "{0}_pods".format(namespace_group)
self.inventory.add_group(name) self.inventory.add_group(name)
self.inventory.add_group(namespace_group) self.inventory.add_group(namespace_group)
@@ -230,12 +251,14 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
for pod in obj.items: for pod in obj.items:
pod_name = pod.metadata.name pod_name = pod.metadata.name
pod_groups = [] pod_groups = []
pod_annotations = {} if not pod.metadata.annotations else dict(pod.metadata.annotations) pod_annotations = (
{} if not pod.metadata.annotations else dict(pod.metadata.annotations)
)
if pod.metadata.labels: if pod.metadata.labels:
# create a group for each label_value # create a group for each label_value
for key, value in pod.metadata.labels: for key, value in pod.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value) group_name = "label_{0}_{1}".format(key, value)
if group_name not in pod_groups: if group_name not in pod_groups:
pod_groups.append(group_name) pod_groups.append(group_name)
self.inventory.add_group(group_name) self.inventory.add_group(group_name)
@@ -248,7 +271,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
for container in pod.status.containerStatuses: for container in pod.status.containerStatuses:
# add each pod_container to the namespace group, and to each label_value group # add each pod_container to the namespace group, and to each label_value group
container_name = '{0}_{1}'.format(pod.metadata.name, container.name) container_name = "{0}_{1}".format(pod.metadata.name, container.name)
self.inventory.add_host(container_name) self.inventory.add_host(container_name)
self.inventory.add_child(namespace_pods_group, container_name) self.inventory.add_child(namespace_pods_group, container_name)
if pod_groups: if pod_groups:
@@ -256,46 +279,85 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
self.inventory.add_child(group, container_name) self.inventory.add_child(group, container_name)
# Add hostvars # Add hostvars
self.inventory.set_variable(container_name, 'object_type', 'pod') self.inventory.set_variable(container_name, "object_type", "pod")
self.inventory.set_variable(container_name, 'labels', pod_labels) self.inventory.set_variable(container_name, "labels", pod_labels)
self.inventory.set_variable(container_name, 'annotations', pod_annotations) self.inventory.set_variable(
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName) container_name, "annotations", pod_annotations
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName) )
self.inventory.set_variable(container_name, 'pod_name', pod.spec.name) self.inventory.set_variable(
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP) container_name, "cluster_name", pod.metadata.clusterName
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase) )
self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP) self.inventory.set_variable(
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink) container_name, "pod_node_name", pod.spec.nodeName
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion) )
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid) self.inventory.set_variable(container_name, "pod_name", pod.spec.name)
self.inventory.set_variable(container_name, 'container_name', container.image) self.inventory.set_variable(
self.inventory.set_variable(container_name, 'container_image', container.image) container_name, "pod_host_ip", pod.status.hostIP
)
self.inventory.set_variable(
container_name, "pod_phase", pod.status.phase
)
self.inventory.set_variable(container_name, "pod_ip", pod.status.podIP)
self.inventory.set_variable(
container_name, "pod_self_link", pod.metadata.selfLink
)
self.inventory.set_variable(
container_name, "pod_resource_version", pod.metadata.resourceVersion
)
self.inventory.set_variable(container_name, "pod_uid", pod.metadata.uid)
self.inventory.set_variable(
container_name, "container_name", container.image
)
self.inventory.set_variable(
container_name, "container_image", container.image
)
if container.state.running: if container.state.running:
self.inventory.set_variable(container_name, 'container_state', 'Running') self.inventory.set_variable(
container_name, "container_state", "Running"
)
if container.state.terminated: if container.state.terminated:
self.inventory.set_variable(container_name, 'container_state', 'Terminated') self.inventory.set_variable(
container_name, "container_state", "Terminated"
)
if container.state.waiting: if container.state.waiting:
self.inventory.set_variable(container_name, 'container_state', 'Waiting') self.inventory.set_variable(
self.inventory.set_variable(container_name, 'container_ready', container.ready) container_name, "container_state", "Waiting"
self.inventory.set_variable(container_name, 'ansible_remote_tmp', '/tmp/') )
self.inventory.set_variable(container_name, 'ansible_connection', self.connection_plugin) self.inventory.set_variable(
self.inventory.set_variable(container_name, 'ansible_{0}_pod'.format(self.transport), container_name, "container_ready", container.ready
pod_name) )
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport), self.inventory.set_variable(
container.name) container_name, "ansible_remote_tmp", "/tmp/"
self.inventory.set_variable(container_name, 'ansible_{0}_namespace'.format(self.transport), )
namespace) self.inventory.set_variable(
container_name, "ansible_connection", self.connection_plugin
)
self.inventory.set_variable(
container_name, "ansible_{0}_pod".format(self.transport), pod_name
)
self.inventory.set_variable(
container_name,
"ansible_{0}_container".format(self.transport),
container.name,
)
self.inventory.set_variable(
container_name,
"ansible_{0}_namespace".format(self.transport),
namespace,
)
def get_services_for_namespace(self, client, name, namespace): def get_services_for_namespace(self, client, name, namespace):
v1_service = client.resources.get(api_version='v1', kind='Service') v1_service = client.resources.get(api_version="v1", kind="Service")
try: try:
obj = v1_service.get(namespace=namespace) obj = v1_service.get(namespace=namespace)
except DynamicApiError as exc: except DynamicApiError as exc:
self.display.debug(exc) self.display.debug(exc)
raise K8sInventoryException('Error fetching Service list: %s' % format_dynamic_api_exc(exc)) raise K8sInventoryException(
"Error fetching Service list: %s" % format_dynamic_api_exc(exc)
)
namespace_group = 'namespace_{0}'.format(namespace) namespace_group = "namespace_{0}".format(namespace)
namespace_services_group = '{0}_services'.format(namespace_group) namespace_services_group = "{0}_services".format(namespace_group)
self.inventory.add_group(name) self.inventory.add_group(name)
self.inventory.add_group(namespace_group) self.inventory.add_group(namespace_group)
@@ -305,15 +367,21 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
for service in obj.items: for service in obj.items:
service_name = service.metadata.name service_name = service.metadata.name
service_labels = {} if not service.metadata.labels else dict(service.metadata.labels) service_labels = (
service_annotations = {} if not service.metadata.annotations else dict(service.metadata.annotations) {} if not service.metadata.labels else dict(service.metadata.labels)
)
service_annotations = (
{}
if not service.metadata.annotations
else dict(service.metadata.annotations)
)
self.inventory.add_host(service_name) self.inventory.add_host(service_name)
if service.metadata.labels: if service.metadata.labels:
# create a group for each label_value # create a group for each label_value
for key, value in service.metadata.labels: for key, value in service.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value) group_name = "label_{0}_{1}".format(key, value)
self.inventory.add_group(group_name) self.inventory.add_group(group_name)
self.inventory.add_child(group_name, service_name) self.inventory.add_child(group_name, service_name)
@@ -322,42 +390,75 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
except AnsibleError: except AnsibleError:
raise raise
ports = [{'name': port.name, ports = [
'port': port.port, {
'protocol': port.protocol, "name": port.name,
'targetPort': port.targetPort, "port": port.port,
'nodePort': port.nodePort} for port in service.spec.ports or []] "protocol": port.protocol,
"targetPort": port.targetPort,
"nodePort": port.nodePort,
}
for port in service.spec.ports or []
]
# add hostvars # add hostvars
self.inventory.set_variable(service_name, 'object_type', 'service') self.inventory.set_variable(service_name, "object_type", "service")
self.inventory.set_variable(service_name, 'labels', service_labels) self.inventory.set_variable(service_name, "labels", service_labels)
self.inventory.set_variable(service_name, 'annotations', service_annotations) self.inventory.set_variable(
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName) service_name, "annotations", service_annotations
self.inventory.set_variable(service_name, 'ports', ports) )
self.inventory.set_variable(service_name, 'type', service.spec.type) self.inventory.set_variable(
self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink) service_name, "cluster_name", service.metadata.clusterName
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion) )
self.inventory.set_variable(service_name, 'uid', service.metadata.uid) self.inventory.set_variable(service_name, "ports", ports)
self.inventory.set_variable(service_name, "type", service.spec.type)
self.inventory.set_variable(
service_name, "self_link", service.metadata.selfLink
)
self.inventory.set_variable(
service_name, "resource_version", service.metadata.resourceVersion
)
self.inventory.set_variable(service_name, "uid", service.metadata.uid)
if service.spec.externalTrafficPolicy: if service.spec.externalTrafficPolicy:
self.inventory.set_variable(service_name, 'external_traffic_policy', self.inventory.set_variable(
service.spec.externalTrafficPolicy) service_name,
"external_traffic_policy",
service.spec.externalTrafficPolicy,
)
if service.spec.externalIPs: if service.spec.externalIPs:
self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs) self.inventory.set_variable(
service_name, "external_ips", service.spec.externalIPs
)
if service.spec.externalName: if service.spec.externalName:
self.inventory.set_variable(service_name, 'external_name', service.spec.externalName) self.inventory.set_variable(
service_name, "external_name", service.spec.externalName
)
if service.spec.healthCheckNodePort: if service.spec.healthCheckNodePort:
self.inventory.set_variable(service_name, 'health_check_node_port', self.inventory.set_variable(
service.spec.healthCheckNodePort) service_name,
"health_check_node_port",
service.spec.healthCheckNodePort,
)
if service.spec.loadBalancerIP: if service.spec.loadBalancerIP:
self.inventory.set_variable(service_name, 'load_balancer_ip', self.inventory.set_variable(
service.spec.loadBalancerIP) service_name, "load_balancer_ip", service.spec.loadBalancerIP
)
if service.spec.selector: if service.spec.selector:
self.inventory.set_variable(service_name, 'selector', dict(service.spec.selector)) self.inventory.set_variable(
service_name, "selector", dict(service.spec.selector)
)
if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress: if (
load_balancer = [{'hostname': ingress.hostname, hasattr(service.status.loadBalancer, "ingress")
'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress] and service.status.loadBalancer.ingress
self.inventory.set_variable(service_name, 'load_balancer', load_balancer) ):
load_balancer = [
{"hostname": ingress.hostname, "ip": ingress.ip}
for ingress in service.status.loadBalancer.ingress
]
self.inventory.set_variable(
service_name, "load_balancer", load_balancer
)

View File

@@ -3,11 +3,11 @@
# #
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = """
lookup: k8s lookup: k8s
short_description: Query the K8s API short_description: Query the K8s API
@@ -117,7 +117,7 @@ DOCUMENTATION = '''
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = """ EXAMPLES = """
- name: Fetch a list of namespaces - name: Fetch a list of namespaces
@@ -187,11 +187,15 @@ from ansible.errors import AnsibleError
from ansible.module_utils.common._collections_compat import KeysView from ansible.module_utils.common._collections_compat import KeysView
from ansible.plugins.lookup import LookupBase from ansible.plugins.lookup import LookupBase
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, get_api_client from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
try: try:
from kubernetes.dynamic.exceptions import NotFoundError from kubernetes.dynamic.exceptions import NotFoundError
HAS_K8S_MODULE_HELPER = True HAS_K8S_MODULE_HELPER = True
k8s_import_exception = None k8s_import_exception = None
except ImportError as e: except ImportError as e:
@@ -200,12 +204,13 @@ except ImportError as e:
class KubernetesLookup(K8sAnsibleMixin): class KubernetesLookup(K8sAnsibleMixin):
def __init__(self): def __init__(self):
if not HAS_K8S_MODULE_HELPER: if not HAS_K8S_MODULE_HELPER:
raise Exception( raise Exception(
"Requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(k8s_import_exception) "Requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(
k8s_import_exception
)
) )
self.kind = None self.kind = None
@@ -226,31 +231,33 @@ class KubernetesLookup(K8sAnsibleMixin):
self.params = kwargs self.params = kwargs
self.client = get_api_client(**kwargs) self.client = get_api_client(**kwargs)
cluster_info = kwargs.get('cluster_info') cluster_info = kwargs.get("cluster_info")
if cluster_info == 'version': if cluster_info == "version":
return [self.client.version] return [self.client.version]
if cluster_info == 'api_groups': if cluster_info == "api_groups":
if isinstance(self.client.resources.api_groups, KeysView): if isinstance(self.client.resources.api_groups, KeysView):
return [list(self.client.resources.api_groups)] return [list(self.client.resources.api_groups)]
return [self.client.resources.api_groups] return [self.client.resources.api_groups]
self.kind = kwargs.get('kind') self.kind = kwargs.get("kind")
self.name = kwargs.get('resource_name') self.name = kwargs.get("resource_name")
self.namespace = kwargs.get('namespace') self.namespace = kwargs.get("namespace")
self.api_version = kwargs.get('api_version', 'v1') self.api_version = kwargs.get("api_version", "v1")
self.label_selector = kwargs.get('label_selector') self.label_selector = kwargs.get("label_selector")
self.field_selector = kwargs.get('field_selector') self.field_selector = kwargs.get("field_selector")
self.include_uninitialized = kwargs.get('include_uninitialized', False) self.include_uninitialized = kwargs.get("include_uninitialized", False)
resource_definition = kwargs.get('resource_definition') resource_definition = kwargs.get("resource_definition")
src = kwargs.get('src') src = kwargs.get("src")
if src: if src:
resource_definition = self.load_resource_definitions(src)[0] resource_definition = self.load_resource_definitions(src)[0]
if resource_definition: if resource_definition:
self.kind = resource_definition.get('kind', self.kind) self.kind = resource_definition.get("kind", self.kind)
self.api_version = resource_definition.get('apiVersion', self.api_version) self.api_version = resource_definition.get("apiVersion", self.api_version)
self.name = resource_definition.get('metadata', {}).get('name', self.name) self.name = resource_definition.get("metadata", {}).get("name", self.name)
self.namespace = resource_definition.get('metadata', {}).get('namespace', self.namespace) self.namespace = resource_definition.get("metadata", {}).get(
"namespace", self.namespace
)
if not self.kind: if not self.kind:
raise AnsibleError( raise AnsibleError(
@@ -260,17 +267,21 @@ class KubernetesLookup(K8sAnsibleMixin):
resource = self.find_resource(self.kind, self.api_version, fail=True) resource = self.find_resource(self.kind, self.api_version, fail=True)
try: try:
k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector) k8s_obj = resource.get(
name=self.name,
namespace=self.namespace,
label_selector=self.label_selector,
field_selector=self.field_selector,
)
except NotFoundError: except NotFoundError:
return [] return []
if self.name: if self.name:
return [k8s_obj.to_dict()] return [k8s_obj.to_dict()]
return k8s_obj.to_dict().get('items') return k8s_obj.to_dict().get("items")
class LookupModule(LookupBase): class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs): def run(self, terms, variables=None, **kwargs):
return KubernetesLookup().run(terms, variables=variables, **kwargs) return KubernetesLookup().run(terms, variables=variables, **kwargs)

View File

@@ -3,7 +3,7 @@
# #
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = ''' DOCUMENTATION = """
lookup: kustomize lookup: kustomize
short_description: Build a set of kubernetes resources using a 'kustomization.yaml' file. short_description: Build a set of kubernetes resources using a 'kustomization.yaml' file.
@@ -33,7 +33,7 @@ DOCUMENTATION = '''
requirements: requirements:
- "python >= 3.6" - "python >= 3.6"
''' """
EXAMPLES = """ EXAMPLES = """
- name: Run lookup using kustomize - name: Run lookup using kustomize
@@ -91,7 +91,7 @@ def get_binary_from_path(name, opt_dirs=None):
if opt_dirs is not None: if opt_dirs is not None:
if not isinstance(opt_dirs, list): if not isinstance(opt_dirs, list):
opt_dirs = [opt_dirs] opt_dirs = [opt_dirs]
opt_arg['opt_dirs'] = opt_dirs opt_arg["opt_dirs"] = opt_dirs
bin_path = get_bin_path(name, **opt_arg) bin_path = get_bin_path(name, **opt_arg)
return bin_path return bin_path
except ValueError: except ValueError:
@@ -104,30 +104,41 @@ def run_command(command):
class LookupModule(LookupBase): class LookupModule(LookupBase):
def run(
def run(self, terms, variables=None, dir=".", binary_path=None, opt_dirs=None, **kwargs): self, terms, variables=None, dir=".", binary_path=None, opt_dirs=None, **kwargs
):
executable_path = binary_path executable_path = binary_path
if executable_path is None: if executable_path is None:
executable_path = get_binary_from_path(name="kustomize", opt_dirs=opt_dirs) executable_path = get_binary_from_path(name="kustomize", opt_dirs=opt_dirs)
if executable_path is None: if executable_path is None:
executable_path = get_binary_from_path(name="kubectl", opt_dirs=opt_dirs) executable_path = get_binary_from_path(
name="kubectl", opt_dirs=opt_dirs
)
# validate that at least one tool was found # validate that at least one tool was found
if executable_path is None: if executable_path is None:
raise AnsibleLookupError("Failed to find required executable 'kubectl' and 'kustomize' in paths") raise AnsibleLookupError(
"Failed to find required executable 'kubectl' and 'kustomize' in paths"
)
# check input directory # check input directory
kustomization_dir = dir kustomization_dir = dir
command = [executable_path] command = [executable_path]
if executable_path.endswith('kustomize'): if executable_path.endswith("kustomize"):
command += ['build', kustomization_dir] command += ["build", kustomization_dir]
elif executable_path.endswith('kubectl'): elif executable_path.endswith("kubectl"):
command += ['kustomize', kustomization_dir] command += ["kustomize", kustomization_dir]
else: else:
raise AnsibleLookupError("unexpected tool provided as parameter {0}, expected one of kustomize, kubectl.".format(executable_path)) raise AnsibleLookupError(
"unexpected tool provided as parameter {0}, expected one of kustomize, kubectl.".format(
executable_path
)
)
(out, err) = run_command(command) (out, err) = run_command(command)
if err: if err:
raise AnsibleLookupError("kustomize command failed with: {0}".format(err.decode("utf-8"))) raise AnsibleLookupError(
return [out.decode('utf-8')] "kustomize command failed with: {0}".format(err.decode("utf-8"))
)
return [out.decode("utf-8")]

View File

@@ -1,4 +1,4 @@
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,6 +17,7 @@ if enable_turbo_mode:
from ansible_collections.cloud.common.plugins.module_utils.turbo.module import ( from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
AnsibleTurboModule as AnsibleModule, AnsibleTurboModule as AnsibleModule,
) # noqa: F401 ) # noqa: F401
AnsibleModule.collection_name = "kubernetes.core" AnsibleModule.collection_name = "kubernetes.core"
except ImportError: except ImportError:
from ansible.module_utils.basic import AnsibleModule # noqa: F401 from ansible.module_utils.basic import AnsibleModule # noqa: F401

View File

@@ -14,13 +14,16 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from collections import OrderedDict from collections import OrderedDict
import json import json
from ansible.module_utils.common.dict_transformations import dict_merge from ansible.module_utils.common.dict_transformations import dict_merge
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import ApplyException from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import (
ApplyException,
)
try: try:
from kubernetes.dynamic.exceptions import NotFoundError from kubernetes.dynamic.exceptions import NotFoundError
@@ -28,50 +31,52 @@ except ImportError:
pass pass
LAST_APPLIED_CONFIG_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration' LAST_APPLIED_CONFIG_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
POD_SPEC_SUFFIXES = { POD_SPEC_SUFFIXES = {
'containers': 'name', "containers": "name",
'initContainers': 'name', "initContainers": "name",
'ephemeralContainers': 'name', "ephemeralContainers": "name",
'volumes': 'name', "volumes": "name",
'imagePullSecrets': 'name', "imagePullSecrets": "name",
'containers.volumeMounts': 'mountPath', "containers.volumeMounts": "mountPath",
'containers.volumeDevices': 'devicePath', "containers.volumeDevices": "devicePath",
'containers.env': 'name', "containers.env": "name",
'containers.ports': 'containerPort', "containers.ports": "containerPort",
'initContainers.volumeMounts': 'mountPath', "initContainers.volumeMounts": "mountPath",
'initContainers.volumeDevices': 'devicePath', "initContainers.volumeDevices": "devicePath",
'initContainers.env': 'name', "initContainers.env": "name",
'initContainers.ports': 'containerPort', "initContainers.ports": "containerPort",
'ephemeralContainers.volumeMounts': 'mountPath', "ephemeralContainers.volumeMounts": "mountPath",
'ephemeralContainers.volumeDevices': 'devicePath', "ephemeralContainers.volumeDevices": "devicePath",
'ephemeralContainers.env': 'name', "ephemeralContainers.env": "name",
'ephemeralContainers.ports': 'containerPort', "ephemeralContainers.ports": "containerPort",
} }
POD_SPEC_PREFIXES = [ POD_SPEC_PREFIXES = [
'Pod.spec', "Pod.spec",
'Deployment.spec.template.spec', "Deployment.spec.template.spec",
'DaemonSet.spec.template.spec', "DaemonSet.spec.template.spec",
'StatefulSet.spec.template.spec', "StatefulSet.spec.template.spec",
'Job.spec.template.spec', "Job.spec.template.spec",
'Cronjob.spec.jobTemplate.spec.template.spec', "Cronjob.spec.jobTemplate.spec.template.spec",
] ]
# patch merge keys taken from generated.proto files under # patch merge keys taken from generated.proto files under
# staging/src/k8s.io/api in kubernetes/kubernetes # staging/src/k8s.io/api in kubernetes/kubernetes
STRATEGIC_MERGE_PATCH_KEYS = { STRATEGIC_MERGE_PATCH_KEYS = {
'Service.spec.ports': 'port', "Service.spec.ports": "port",
'ServiceAccount.secrets': 'name', "ServiceAccount.secrets": "name",
'ValidatingWebhookConfiguration.webhooks': 'name', "ValidatingWebhookConfiguration.webhooks": "name",
'MutatingWebhookConfiguration.webhooks': 'name', "MutatingWebhookConfiguration.webhooks": "name",
} }
STRATEGIC_MERGE_PATCH_KEYS.update( STRATEGIC_MERGE_PATCH_KEYS.update(
{"%s.%s" % (prefix, key): value {
for prefix in POD_SPEC_PREFIXES "%s.%s" % (prefix, key): value
for key, value in POD_SPEC_SUFFIXES.items()} for prefix in POD_SPEC_PREFIXES
for key, value in POD_SPEC_SUFFIXES.items()
}
) )
@@ -79,21 +84,28 @@ def annotate(desired):
return dict( return dict(
metadata=dict( metadata=dict(
annotations={ annotations={
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(desired, separators=(',', ':'), indent=None, sort_keys=True) LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(
desired, separators=(",", ":"), indent=None, sort_keys=True
)
} }
) )
) )
def apply_patch(actual, desired): def apply_patch(actual, desired):
last_applied = actual['metadata'].get('annotations', {}).get(LAST_APPLIED_CONFIG_ANNOTATION) last_applied = (
actual["metadata"].get("annotations", {}).get(LAST_APPLIED_CONFIG_ANNOTATION)
)
if last_applied: if last_applied:
# ensure that last_applied doesn't come back as a dict of unicode key/value pairs # ensure that last_applied doesn't come back as a dict of unicode key/value pairs
# json.loads can be used if we stop supporting python 2 # json.loads can be used if we stop supporting python 2
last_applied = json.loads(last_applied) last_applied = json.loads(last_applied)
patch = merge(dict_merge(last_applied, annotate(last_applied)), patch = merge(
dict_merge(desired, annotate(desired)), actual) dict_merge(last_applied, annotate(last_applied)),
dict_merge(desired, annotate(desired)),
actual,
)
if patch: if patch:
return actual, patch return actual, patch
else: else:
@@ -104,7 +116,10 @@ def apply_patch(actual, desired):
def apply_object(resource, definition): def apply_object(resource, definition):
try: try:
actual = resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace')) actual = resource.get(
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
)
except NotFoundError: except NotFoundError:
return None, dict_merge(definition, annotate(definition)) return None, dict_merge(definition, annotate(definition))
return apply_patch(actual.to_dict(), definition) return apply_patch(actual.to_dict(), definition)
@@ -113,14 +128,21 @@ def apply_object(resource, definition):
def k8s_apply(resource, definition, **kwargs): def k8s_apply(resource, definition, **kwargs):
existing, desired = apply_object(resource, definition) existing, desired = apply_object(resource, definition)
if not existing: if not existing:
return resource.create(body=desired, namespace=definition['metadata'].get('namespace'), **kwargs) return resource.create(
body=desired, namespace=definition["metadata"].get("namespace"), **kwargs
)
if existing == desired: if existing == desired:
return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace')) return resource.get(
return resource.patch(body=desired, name=definition["metadata"]["name"],
name=definition['metadata']['name'], namespace=definition["metadata"].get("namespace"),
namespace=definition['metadata'].get('namespace'), )
content_type='application/merge-patch+json', return resource.patch(
**kwargs) body=desired,
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
content_type="application/merge-patch+json",
**kwargs
)
# The patch is the difference from actual to desired without deletions, plus deletions # The patch is the difference from actual to desired without deletions, plus deletions
@@ -129,7 +151,7 @@ def k8s_apply(resource, definition, **kwargs):
# deletions, and then apply delta to deletions as a patch, which should be strictly additive. # deletions, and then apply delta to deletions as a patch, which should be strictly additive.
def merge(last_applied, desired, actual, position=None): def merge(last_applied, desired, actual, position=None):
deletions = get_deletions(last_applied, desired) deletions = get_deletions(last_applied, desired)
delta = get_delta(last_applied, actual, desired, position or desired['kind']) delta = get_delta(last_applied, actual, desired, position or desired["kind"])
return dict_merge(deletions, delta) return dict_merge(deletions, delta)
@@ -139,7 +161,9 @@ def list_to_dict(lst, key, position):
try: try:
result[item[key]] = item result[item[key]] = item
except KeyError: except KeyError:
raise ApplyException("Expected key '%s' not found in position %s" % (key, position)) raise ApplyException(
"Expected key '%s' not found in position %s" % (key, position)
)
return result return result
@@ -158,7 +182,12 @@ def list_merge(last_applied, actual, desired, position):
if key not in actual_dict or key not in last_applied_dict: if key not in actual_dict or key not in last_applied_dict:
result.append(desired_dict[key]) result.append(desired_dict[key])
else: else:
patch = merge(last_applied_dict[key], desired_dict[key], actual_dict[key], position) patch = merge(
last_applied_dict[key],
desired_dict[key],
actual_dict[key],
position,
)
result.append(dict_merge(actual_dict[key], patch)) result.append(dict_merge(actual_dict[key], patch))
for key in actual_dict: for key in actual_dict:
if key not in desired_dict and key not in last_applied_dict: if key not in desired_dict and key not in last_applied_dict:
@@ -198,11 +227,11 @@ def recursive_list_diff(list1, list2, position=None):
def recursive_diff(dict1, dict2, position=None): def recursive_diff(dict1, dict2, position=None):
if not position: if not position:
if 'kind' in dict1 and dict1.get('kind') == dict2.get('kind'): if "kind" in dict1 and dict1.get("kind") == dict2.get("kind"):
position = dict1['kind'] position = dict1["kind"]
left = dict((k, v) for (k, v) in dict1.items() if k not in dict2) left = dict((k, v) for (k, v) in dict1.items() if k not in dict2)
right = dict((k, v) for (k, v) in dict2.items() if k not in dict1) right = dict((k, v) for (k, v) in dict2.items() if k not in dict1)
for k in (set(dict1.keys()) & set(dict2.keys())): for k in set(dict1.keys()) & set(dict2.keys()):
if position: if position:
this_position = "%s.%s" % (position, k) this_position = "%s.%s" % (position, k)
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict): if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
@@ -247,11 +276,15 @@ def get_delta(last_applied, actual, desired, position=None):
if actual_value is None: if actual_value is None:
patch[k] = desired_value patch[k] = desired_value
elif isinstance(desired_value, dict): elif isinstance(desired_value, dict):
p = get_delta(last_applied.get(k, {}), actual_value, desired_value, this_position) p = get_delta(
last_applied.get(k, {}), actual_value, desired_value, this_position
)
if p: if p:
patch[k] = p patch[k] = p
elif isinstance(desired_value, list): elif isinstance(desired_value, list):
p = list_merge(last_applied.get(k, []), actual_value, desired_value, this_position) p = list_merge(
last_applied.get(k, []), actual_value, desired_value, this_position
)
if p: if p:
patch[k] = [item for item in p if item is not None] patch[k] = [item for item in p if item is not None]
elif actual_value != desired_value: elif actual_value != desired_value:

View File

@@ -1,4 +1,4 @@
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
@@ -12,133 +12,83 @@ def list_dict_str(value):
AUTH_PROXY_HEADERS_SPEC = dict( AUTH_PROXY_HEADERS_SPEC = dict(
proxy_basic_auth=dict(type='str', no_log=True), proxy_basic_auth=dict(type="str", no_log=True),
basic_auth=dict(type='str', no_log=True), basic_auth=dict(type="str", no_log=True),
user_agent=dict(type='str') user_agent=dict(type="str"),
) )
AUTH_ARG_SPEC = { AUTH_ARG_SPEC = {
'kubeconfig': { "kubeconfig": {"type": "raw"},
'type': 'raw', "context": {},
}, "host": {},
'context': {}, "api_key": {"no_log": True},
'host': {}, "username": {},
'api_key': { "password": {"no_log": True},
'no_log': True, "validate_certs": {"type": "bool", "aliases": ["verify_ssl"]},
}, "ca_cert": {"type": "path", "aliases": ["ssl_ca_cert"]},
'username': {}, "client_cert": {"type": "path", "aliases": ["cert_file"]},
'password': { "client_key": {"type": "path", "aliases": ["key_file"]},
'no_log': True, "proxy": {"type": "str"},
}, "proxy_headers": {"type": "dict", "options": AUTH_PROXY_HEADERS_SPEC},
'validate_certs': { "persist_config": {"type": "bool"},
'type': 'bool',
'aliases': ['verify_ssl'],
},
'ca_cert': {
'type': 'path',
'aliases': ['ssl_ca_cert'],
},
'client_cert': {
'type': 'path',
'aliases': ['cert_file'],
},
'client_key': {
'type': 'path',
'aliases': ['key_file'],
},
'proxy': {
'type': 'str',
},
'proxy_headers': {
'type': 'dict',
'options': AUTH_PROXY_HEADERS_SPEC
},
'persist_config': {
'type': 'bool',
},
} }
WAIT_ARG_SPEC = dict( WAIT_ARG_SPEC = dict(
wait=dict(type='bool', default=False), wait=dict(type="bool", default=False),
wait_sleep=dict(type='int', default=5), wait_sleep=dict(type="int", default=5),
wait_timeout=dict(type='int', default=120), wait_timeout=dict(type="int", default=120),
wait_condition=dict( wait_condition=dict(
type='dict', type="dict",
default=None, default=None,
options=dict( options=dict(
type=dict(), type=dict(),
status=dict(default=True, choices=[True, False, "Unknown"]), status=dict(default=True, choices=[True, False, "Unknown"]),
reason=dict() reason=dict(),
) ),
) ),
) )
# Map kubernetes-client parameters to ansible parameters # Map kubernetes-client parameters to ansible parameters
AUTH_ARG_MAP = { AUTH_ARG_MAP = {
'kubeconfig': 'kubeconfig', "kubeconfig": "kubeconfig",
'context': 'context', "context": "context",
'host': 'host', "host": "host",
'api_key': 'api_key', "api_key": "api_key",
'username': 'username', "username": "username",
'password': 'password', "password": "password",
'verify_ssl': 'validate_certs', "verify_ssl": "validate_certs",
'ssl_ca_cert': 'ca_cert', "ssl_ca_cert": "ca_cert",
'cert_file': 'client_cert', "cert_file": "client_cert",
'key_file': 'client_key', "key_file": "client_key",
'proxy': 'proxy', "proxy": "proxy",
'proxy_headers': 'proxy_headers', "proxy_headers": "proxy_headers",
'persist_config': 'persist_config', "persist_config": "persist_config",
} }
NAME_ARG_SPEC = { NAME_ARG_SPEC = {
'kind': {}, "kind": {},
'name': {}, "name": {},
'namespace': {}, "namespace": {},
'api_version': { "api_version": {"default": "v1", "aliases": ["api", "version"]},
'default': 'v1',
'aliases': ['api', 'version'],
},
} }
COMMON_ARG_SPEC = { COMMON_ARG_SPEC = {
'state': { "state": {"default": "present", "choices": ["present", "absent"]},
'default': 'present', "force": {"type": "bool", "default": False},
'choices': ['present', 'absent'],
},
'force': {
'type': 'bool',
'default': False,
},
} }
RESOURCE_ARG_SPEC = { RESOURCE_ARG_SPEC = {
'resource_definition': { "resource_definition": {"type": list_dict_str, "aliases": ["definition", "inline"]},
'type': list_dict_str, "src": {"type": "path"},
'aliases': ['definition', 'inline']
},
'src': {
'type': 'path',
},
} }
ARG_ATTRIBUTES_BLACKLIST = ('property_path',) ARG_ATTRIBUTES_BLACKLIST = ("property_path",)
DELETE_OPTS_ARG_SPEC = { DELETE_OPTS_ARG_SPEC = {
'propagationPolicy': { "propagationPolicy": {"choices": ["Foreground", "Background", "Orphan"]},
'choices': ['Foreground', 'Background', 'Orphan'], "gracePeriodSeconds": {"type": "int"},
"preconditions": {
"type": "dict",
"options": {"resourceVersion": {"type": "str"}, "uid": {"type": "str"}},
}, },
'gracePeriodSeconds': {
'type': 'int',
},
'preconditions': {
'type': 'dict',
'options': {
'resourceVersion': {
'type': 'str',
},
'uid': {
'type': 'str',
}
}
}
} }

View File

@@ -23,17 +23,26 @@ from functools import partial
import kubernetes.dynamic import kubernetes.dynamic
import kubernetes.dynamic.discovery import kubernetes.dynamic.discovery
from kubernetes import __version__ from kubernetes import __version__
from kubernetes.dynamic.exceptions import (ResourceNotFoundError, ResourceNotUniqueError, from kubernetes.dynamic.exceptions import (
ServiceUnavailableError) ResourceNotFoundError,
ResourceNotUniqueError,
ServiceUnavailableError,
)
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
ResourceList,
)
class Discoverer(kubernetes.dynamic.discovery.Discoverer): class Discoverer(kubernetes.dynamic.discovery.Discoverer):
def __init__(self, client, cache_file): def __init__(self, client, cache_file):
self.client = client self.client = client
default_cache_file_name = 'k8srcp-{0}.json'.format(hashlib.sha256(self.__get_default_cache_id()).hexdigest()) default_cache_file_name = "k8srcp-{0}.json".format(
self.__cache_file = cache_file or os.path.join(tempfile.gettempdir(), default_cache_file_name) hashlib.sha256(self.__get_default_cache_id()).hexdigest()
)
self.__cache_file = cache_file or os.path.join(
tempfile.gettempdir(), default_cache_file_name
)
self.__init_cache() self.__init_cache()
def __get_default_cache_id(self): def __get_default_cache_id(self):
@@ -42,21 +51,21 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
cache_id = "{0}-{1}".format(self.client.configuration.host, user) cache_id = "{0}-{1}".format(self.client.configuration.host, user)
else: else:
cache_id = self.client.configuration.host cache_id = self.client.configuration.host
return cache_id.encode('utf-8') return cache_id.encode("utf-8")
def __get_user(self): def __get_user(self):
# This is intended to provide a portable method for getting a username. # This is intended to provide a portable method for getting a username.
# It could, and maybe should, be replaced by getpass.getuser() but, due # It could, and maybe should, be replaced by getpass.getuser() but, due
# to a lack of portability testing the original code is being left in # to a lack of portability testing the original code is being left in
# place. # place.
if hasattr(os, 'getlogin'): if hasattr(os, "getlogin"):
try: try:
user = os.getlogin() user = os.getlogin()
if user: if user:
return str(user) return str(user)
except OSError: except OSError:
pass pass
if hasattr(os, 'getuid'): if hasattr(os, "getuid"):
try: try:
user = os.getuid() user = os.getuid()
if user: if user:
@@ -70,13 +79,13 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
def __init_cache(self, refresh=False): def __init_cache(self, refresh=False):
if refresh or not os.path.exists(self.__cache_file): if refresh or not os.path.exists(self.__cache_file):
self._cache = {'library_version': __version__} self._cache = {"library_version": __version__}
refresh = True refresh = True
else: else:
try: try:
with open(self.__cache_file, 'r') as f: with open(self.__cache_file, "r") as f:
self._cache = json.load(f, cls=partial(CacheDecoder, self.client)) self._cache = json.load(f, cls=partial(CacheDecoder, self.client))
if self._cache.get('library_version') != __version__: if self._cache.get("library_version") != __version__:
# Version mismatch, need to refresh cache # Version mismatch, need to refresh cache
self.invalidate_cache() self.invalidate_cache()
except Exception: except Exception:
@@ -92,21 +101,25 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
resources = defaultdict(list) resources = defaultdict(list)
subresources = defaultdict(dict) subresources = defaultdict(dict)
path = '/'.join(filter(None, [prefix, group, version])) path = "/".join(filter(None, [prefix, group, version]))
try: try:
resources_response = self.client.request('GET', path).resources or [] resources_response = self.client.request("GET", path).resources or []
except ServiceUnavailableError: except ServiceUnavailableError:
resources_response = [] resources_response = []
resources_raw = list(filter(lambda resource: '/' not in resource['name'], resources_response)) resources_raw = list(
subresources_raw = list(filter(lambda resource: '/' in resource['name'], resources_response)) filter(lambda resource: "/" not in resource["name"], resources_response)
)
subresources_raw = list(
filter(lambda resource: "/" in resource["name"], resources_response)
)
for subresource in subresources_raw: for subresource in subresources_raw:
resource, name = subresource['name'].split('/') resource, name = subresource["name"].split("/")
subresources[resource][name] = subresource subresources[resource][name] = subresource
for resource in resources_raw: for resource in resources_raw:
# Prevent duplicate keys # Prevent duplicate keys
for key in ('prefix', 'group', 'api_version', 'client', 'preferred'): for key in ("prefix", "group", "api_version", "client", "preferred"):
resource.pop(key, None) resource.pop(key, None)
resourceobj = kubernetes.dynamic.Resource( resourceobj = kubernetes.dynamic.Resource(
@@ -115,19 +128,25 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
api_version=version, api_version=version,
client=self.client, client=self.client,
preferred=preferred, preferred=preferred,
subresources=subresources.get(resource['name']), subresources=subresources.get(resource["name"]),
**resource **resource
) )
resources[resource['kind']].append(resourceobj) resources[resource["kind"]].append(resourceobj)
resource_lookup = { resource_lookup = {
'prefix': prefix, "prefix": prefix,
'group': group, "group": group,
'api_version': version, "api_version": version,
'kind': resourceobj.kind, "kind": resourceobj.kind,
'name': resourceobj.name "name": resourceobj.name,
} }
resource_list = ResourceList(self.client, group=group, api_version=version, base_kind=resource['kind'], base_resource_lookup=resource_lookup) resource_list = ResourceList(
self.client,
group=group,
api_version=version,
base_kind=resource["kind"],
base_resource_lookup=resource_lookup,
)
resources[resource_list.kind].append(resource_list) resources[resource_list.kind].append(resource_list)
return resources return resources
@@ -139,23 +158,32 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
""" """
results = self.search(**kwargs) results = self.search(**kwargs)
# If there are multiple matches, prefer exact matches on api_version # If there are multiple matches, prefer exact matches on api_version
if len(results) > 1 and kwargs.get('api_version'): if len(results) > 1 and kwargs.get("api_version"):
results = [ results = [
result for result in results if result.group_version == kwargs['api_version'] result
for result in results
if result.group_version == kwargs["api_version"]
] ]
# If there are multiple matches, prefer non-List kinds # If there are multiple matches, prefer non-List kinds
if len(results) > 1 and not all(isinstance(x, ResourceList) for x in results): if len(results) > 1 and not all(isinstance(x, ResourceList) for x in results):
results = [result for result in results if not isinstance(result, ResourceList)] results = [
result for result in results if not isinstance(result, ResourceList)
]
# if multiple resources are found that share a GVK, prefer the one with the most supported verbs # if multiple resources are found that share a GVK, prefer the one with the most supported verbs
if len(results) > 1 and len(set((x.group_version, x.kind) for x in results)) == 1: if (
len(results) > 1
and len(set((x.group_version, x.kind) for x in results)) == 1
):
if len(set(len(x.verbs) for x in results)) != 1: if len(set(len(x.verbs) for x in results)) != 1:
results = [max(results, key=lambda x: len(x.verbs))] results = [max(results, key=lambda x: len(x.verbs))]
if len(results) == 1: if len(results) == 1:
return results[0] return results[0]
elif not results: elif not results:
raise ResourceNotFoundError('No matches found for {0}'.format(kwargs)) raise ResourceNotFoundError("No matches found for {0}".format(kwargs))
else: else:
raise ResourceNotUniqueError('Multiple matches found for {0}: {1}'.format(kwargs, results)) raise ResourceNotUniqueError(
"Multiple matches found for {0}: {1}".format(kwargs, results)
)
class LazyDiscoverer(Discoverer, kubernetes.dynamic.LazyDiscoverer): class LazyDiscoverer(Discoverer, kubernetes.dynamic.LazyDiscoverer):
@@ -174,13 +202,15 @@ class CacheDecoder(json.JSONDecoder):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, obj): def object_hook(self, obj):
if '_type' not in obj: if "_type" not in obj:
return obj return obj
_type = obj.pop('_type') _type = obj.pop("_type")
if _type == 'Resource': if _type == "Resource":
return kubernetes.dynamic.Resource(client=self.client, **obj) return kubernetes.dynamic.Resource(client=self.client, **obj)
elif _type == 'ResourceList': elif _type == "ResourceList":
return ResourceList(self.client, **obj) return ResourceList(self.client, **obj)
elif _type == 'ResourceGroup': elif _type == "ResourceGroup":
return kubernetes.dynamic.discovery.ResourceGroup(obj['preferred'], resources=self.object_hook(obj['resources'])) return kubernetes.dynamic.discovery.ResourceGroup(
obj["preferred"], resources=self.object_hook(obj["resources"])
)
return obj return obj

View File

@@ -14,6 +14,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -21,11 +22,19 @@ import kubernetes.dynamic
class ResourceList(kubernetes.dynamic.resource.ResourceList): class ResourceList(kubernetes.dynamic.resource.ResourceList):
def __init__(self, client, group='', api_version='v1', base_kind='', kind=None, base_resource_lookup=None): def __init__(
self,
client,
group="",
api_version="v1",
base_kind="",
kind=None,
base_resource_lookup=None,
):
self.client = client self.client = client
self.group = group self.group = group
self.api_version = api_version self.api_version = api_version
self.kind = kind or '{0}List'.format(base_kind) self.kind = kind or "{0}List".format(base_kind)
self.base_kind = base_kind self.base_kind = base_kind
self.base_resource_lookup = base_resource_lookup self.base_resource_lookup = base_resource_lookup
self.__base_resource = None self.__base_resource = None
@@ -34,16 +43,18 @@ class ResourceList(kubernetes.dynamic.resource.ResourceList):
if self.__base_resource: if self.__base_resource:
return self.__base_resource return self.__base_resource
elif self.base_resource_lookup: elif self.base_resource_lookup:
self.__base_resource = self.client.resources.get(**self.base_resource_lookup) self.__base_resource = self.client.resources.get(
**self.base_resource_lookup
)
return self.__base_resource return self.__base_resource
return None return None
def to_dict(self): def to_dict(self):
return { return {
'_type': 'ResourceList', "_type": "ResourceList",
'group': self.group, "group": self.group,
'api_version': self.api_version, "api_version": self.api_version,
'kind': self.kind, "kind": self.kind,
'base_kind': self.base_kind, "base_kind": self.base_kind,
'base_resource_lookup': self.base_resource_lookup "base_resource_lookup": self.base_resource_lookup,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -15,7 +15,8 @@
# Implement ConfigMapHash and SecretHash equivalents # Implement ConfigMapHash and SecretHash equivalents
# Based on https://github.com/kubernetes/kubernetes/pull/49961 # Based on https://github.com/kubernetes/kubernetes/pull/49961
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import json import json
@@ -23,6 +24,7 @@ import hashlib
try: try:
import string import string
maketrans = string.maketrans maketrans = string.maketrans
except AttributeError: except AttributeError:
maketrans = str.maketrans maketrans = str.maketrans
@@ -44,21 +46,21 @@ def sorted_dict(unsorted_dict):
def generate_hash(resource): def generate_hash(resource):
# Get name from metadata # Get name from metadata
metada = resource.get('metadata', {}) metada = resource.get("metadata", {})
key = 'name' key = "name"
resource['name'] = metada.get('name', '') resource["name"] = metada.get("name", "")
generate_name = metada.get('generateName', '') generate_name = metada.get("generateName", "")
if resource['name'] == '' and generate_name: if resource["name"] == "" and generate_name:
del(resource['name']) del resource["name"]
key = 'generateName' key = "generateName"
resource['generateName'] = generate_name resource["generateName"] = generate_name
if resource['kind'] == 'ConfigMap': if resource["kind"] == "ConfigMap":
marshalled = marshal(sorted_dict(resource), ['data', 'kind', key]) marshalled = marshal(sorted_dict(resource), ["data", "kind", key])
del(resource[key]) del resource[key]
return encode(marshalled) return encode(marshalled)
if resource['kind'] == 'Secret': if resource["kind"] == "Secret":
marshalled = marshal(sorted_dict(resource), ['data', 'kind', key, 'type']) marshalled = marshal(sorted_dict(resource), ["data", "kind", key, "type"])
del(resource[key]) del resource[key]
return encode(marshalled) return encode(marshalled)
raise NotImplementedError raise NotImplementedError
@@ -67,8 +69,10 @@ def marshal(data, keys):
ordered = OrderedDict() ordered = OrderedDict()
for key in keys: for key in keys:
ordered[key] = data.get(key, "") ordered[key] = data.get(key, "")
return json.dumps(ordered, separators=(',', ':')).encode('utf-8') return json.dumps(ordered, separators=(",", ":")).encode("utf-8")
def encode(resource): def encode(resource):
return hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt")) return (
hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt"))
)

View File

@@ -18,6 +18,7 @@ from ansible.module_utils.basic import missing_required_lib
try: try:
import yaml import yaml
HAS_YAML = True HAS_YAML = True
except ImportError: except ImportError:
YAML_IMP_ERR = traceback.format_exc() YAML_IMP_ERR = traceback.format_exc()
@@ -28,11 +29,11 @@ except ImportError:
def prepare_helm_environ_update(module): def prepare_helm_environ_update(module):
environ_update = {} environ_update = {}
file_to_cleam_up = None file_to_cleam_up = None
kubeconfig_path = module.params.get('kubeconfig') kubeconfig_path = module.params.get("kubeconfig")
if module.params.get('context') is not None: if module.params.get("context") is not None:
environ_update["HELM_KUBECONTEXT"] = module.params.get('context') environ_update["HELM_KUBECONTEXT"] = module.params.get("context")
if module.params.get('release_namespace'): if module.params.get("release_namespace"):
environ_update["HELM_NAMESPACE"] = module.params.get('release_namespace') environ_update["HELM_NAMESPACE"] = module.params.get("release_namespace")
if module.params.get("api_key"): if module.params.get("api_key"):
environ_update["HELM_KUBETOKEN"] = module.params["api_key"] environ_update["HELM_KUBETOKEN"] = module.params["api_key"]
if module.params.get("host"): if module.params.get("host"):
@@ -41,7 +42,8 @@ def prepare_helm_environ_update(module):
kubeconfig_path = write_temp_kubeconfig( kubeconfig_path = write_temp_kubeconfig(
module.params["host"], module.params["host"],
validate_certs=module.params["validate_certs"], validate_certs=module.params["validate_certs"],
ca_cert=module.params["ca_cert"]) ca_cert=module.params["ca_cert"],
)
file_to_cleam_up = kubeconfig_path file_to_cleam_up = kubeconfig_path
if kubeconfig_path is not None: if kubeconfig_path is not None:
environ_update["KUBECONFIG"] = kubeconfig_path environ_update["KUBECONFIG"] = kubeconfig_path
@@ -61,7 +63,9 @@ def run_helm(module, command, fails_on_error=True):
rc, out, err = module.run_command(command, environ_update=environ_update) rc, out, err = module.run_command(command, environ_update=environ_update)
if fails_on_error and rc != 0: if fails_on_error and rc != 0:
module.fail_json( module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
rc, out, err
),
stdout=out, stdout=out,
stderr=err, stderr=err,
command=command, command=command,
@@ -90,23 +94,11 @@ def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None):
content = { content = {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Config", "kind": "Config",
"clusters": [ "clusters": [{"cluster": {"server": server}, "name": "generated-cluster"}],
{
"cluster": {
"server": server,
},
"name": "generated-cluster"
}
],
"contexts": [ "contexts": [
{ {"context": {"cluster": "generated-cluster"}, "name": "generated-context"}
"context": {
"cluster": "generated-cluster"
},
"name": "generated-context"
}
], ],
"current-context": "generated-context" "current-context": "generated-context",
} }
if not validate_certs: if not validate_certs:
@@ -115,7 +107,7 @@ def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None):
content["clusters"][0]["cluster"]["certificate-authority"] = ca_cert content["clusters"][0]["cluster"]["certificate-authority"] = ca_cert
_fd, file_name = tempfile.mkstemp() _fd, file_name = tempfile.mkstemp()
with os.fdopen(_fd, 'w') as fp: with os.fdopen(_fd, "w") as fp:
yaml.dump(content, fp) yaml.dump(content, fp)
return file_name return file_name
@@ -128,7 +120,7 @@ def get_helm_plugin_list(module, helm_bin=None):
return [] return []
helm_plugin_list = helm_bin + " list" helm_plugin_list = helm_bin + " list"
rc, out, err = run_helm(module, helm_plugin_list) rc, out, err = run_helm(module, helm_plugin_list)
if rc != 0 or (out == '' and err == ''): if rc != 0 or (out == "" and err == ""):
module.fail_json( module.fail_json(
msg="Failed to get Helm plugin info", msg="Failed to get Helm plugin info",
command=helm_plugin_list, command=helm_plugin_list,
@@ -150,11 +142,11 @@ def parse_helm_plugin_list(module, output=None):
for line in output: for line in output:
if line.startswith("NAME"): if line.startswith("NAME"):
continue continue
name, version, description = line.split('\t', 3) name, version, description = line.split("\t", 3)
name = name.strip() name = name.strip()
version = version.strip() version = version.strip()
description = description.strip() description = description.strip()
if name == '': if name == "":
continue continue
ret.append((name, version, description)) ret.append((name, version, description))

View File

@@ -14,26 +14,37 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from kubernetes.dynamic import DynamicClient from kubernetes.dynamic import DynamicClient
from ansible_collections.kubernetes.core.plugins.module_utils.apply import k8s_apply from ansible_collections.kubernetes.core.plugins.module_utils.apply import k8s_apply
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import ApplyException from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import (
ApplyException,
)
class K8SDynamicClient(DynamicClient): class K8SDynamicClient(DynamicClient):
def apply(self, resource, body=None, name=None, namespace=None, **kwargs): def apply(self, resource, body=None, name=None, namespace=None, **kwargs):
body = super().serialize_body(body) body = super().serialize_body(body)
body['metadata'] = body.get('metadata', dict()) body["metadata"] = body.get("metadata", dict())
name = name or body['metadata'].get('name') name = name or body["metadata"].get("name")
if not name: if not name:
raise ValueError("name is required to apply {0}.{1}".format(resource.group_version, resource.kind)) raise ValueError(
"name is required to apply {0}.{1}".format(
resource.group_version, resource.kind
)
)
if resource.namespaced: if resource.namespaced:
body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body) body["metadata"]["namespace"] = super().ensure_namespace(
resource, namespace, body
)
try: try:
return k8s_apply(resource, body, **kwargs) return k8s_apply(resource, body, **kwargs)
except ApplyException as e: except ApplyException as e:
raise ValueError("Could not apply strategic merge to %s/%s: %s" % raise ValueError(
(body['kind'], body['metadata']['name'], e)) "Could not apply strategic merge to %s/%s: %s"
% (body["kind"], body["metadata"]["name"], e)
)

View File

@@ -17,7 +17,7 @@ import re
class Selector(object): class Selector(object):
equality_based_operators = ('==', '!=', '=') equality_based_operators = ("==", "!=", "=")
def __init__(self, data): def __init__(self, data):
self._operator = None self._operator = None
@@ -27,18 +27,23 @@ class Selector(object):
for op in self.equality_based_operators: for op in self.equality_based_operators:
idx = no_whitespace_data.find(op) idx = no_whitespace_data.find(op)
if idx != -1: if idx != -1:
self._operator = "in" if op == '==' or op == '=' else "notin" self._operator = "in" if op == "==" or op == "=" else "notin"
self._key = no_whitespace_data[0:idx] self._key = no_whitespace_data[0:idx]
# fmt: off
self._data = [no_whitespace_data[idx + len(op):]] self._data = [no_whitespace_data[idx + len(op):]]
# fmt: on
break break
def parse_set_based_requirement(self, data): def parse_set_based_requirement(self, data):
m = re.match(r'( *)([a-z0-9A-Z][a-z0-9A-Z\._-]*[a-z0-9A-Z])( +)(notin|in)( +)\((.*)\)( *)', data) m = re.match(
r"( *)([a-z0-9A-Z][a-z0-9A-Z\._-]*[a-z0-9A-Z])( +)(notin|in)( +)\((.*)\)( *)",
data,
)
if m: if m:
self._set_based_requirement = True self._set_based_requirement = True
self._key = m.group(2) self._key = m.group(2)
self._operator = m.group(4) self._operator = m.group(4)
self._data = [x.replace(' ', '') for x in m.group(6).split(',') if x != ''] self._data = [x.replace(" ", "") for x in m.group(6).split(",") if x != ""]
return True return True
elif all(x not in data for x in self.equality_based_operators): elif all(x not in data for x in self.equality_based_operators):
self._key = data.rstrip(" ").lstrip(" ") self._key = data.rstrip(" ").lstrip(" ")
@@ -54,18 +59,21 @@ class Selector(object):
elif self._operator == "notin": elif self._operator == "notin":
return self._key not in labels or labels.get(self._key) not in self._data return self._key not in labels or labels.get(self._key) not in self._data
else: else:
return self._key not in labels if self._operator == "!" else self._key in labels return (
self._key not in labels
if self._operator == "!"
else self._key in labels
)
class LabelSelectorFilter(object): class LabelSelectorFilter(object):
def __init__(self, label_selectors): def __init__(self, label_selectors):
self.selectors = [Selector(data) for data in label_selectors] self.selectors = [Selector(data) for data in label_selectors]
def isMatching(self, definition): def isMatching(self, definition):
if "metadata" not in definition or "labels" not in definition['metadata']: if "metadata" not in definition or "labels" not in definition["metadata"]:
return False return False
labels = definition['metadata']['labels'] labels = definition["metadata"]["labels"]
if not isinstance(labels, dict): if not isinstance(labels, dict):
return None return None
return all(sel.isMatch(labels) for sel in self.selectors) return all(sel.isMatch(labels) for sel in self.selectors)

View File

@@ -4,10 +4,11 @@
# Copyright: Ansible Project # Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
--- ---
module: helm module: helm
@@ -159,9 +160,9 @@ options:
version_added: "2.2.0" version_added: "2.2.0"
extends_documentation_fragment: extends_documentation_fragment:
- kubernetes.core.helm_common_options - kubernetes.core.helm_common_options
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Deploy latest version of Prometheus chart inside monitoring namespace (and create it) - name: Deploy latest version of Prometheus chart inside monitoring namespace (and create it)
kubernetes.core.helm: kubernetes.core.helm:
name: test name: test
@@ -248,7 +249,7 @@ EXAMPLES = r'''
enabled: True enabled: True
logging: logging:
enabled: True enabled: True
''' """
RETURN = r""" RETURN = r"""
status: status:
@@ -311,6 +312,7 @@ from distutils.version import LooseVersion
try: try:
import yaml import yaml
IMP_YAML = True IMP_YAML = True
except ImportError: except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
@@ -333,7 +335,7 @@ def get_release(state, release_name):
if state is not None: if state is not None:
for release in state: for release in state:
if release['name'] == release_name: if release["name"] == release_name:
return release return release
return None return None
@@ -352,7 +354,7 @@ def get_release_status(module, command, release_name):
if release is None: # not install if release is None: # not install
return None return None
release['values'] = get_values(module, command, release_name) release["values"] = get_values(module, command, release_name)
return release return release
@@ -376,9 +378,23 @@ def fetch_chart_info(module, command, chart_ref):
return yaml.safe_load(out) return yaml.safe_load(out)
def deploy(command, release_name, release_values, chart_name, wait, def deploy(
wait_timeout, disable_hook, force, values_files, history_max, atomic=False, command,
create_namespace=False, replace=False, skip_crds=False, timeout=None): release_name,
release_values,
chart_name,
wait,
wait_timeout,
disable_hook,
force,
values_files,
history_max,
atomic=False,
create_namespace=False,
replace=False,
skip_crds=False,
timeout=None,
):
""" """
Install/upgrade/rollback release chart Install/upgrade/rollback release chart
""" """
@@ -419,8 +435,8 @@ def deploy(command, release_name, release_values, chart_name, wait,
deploy_command += " --values=" + value_file deploy_command += " --values=" + value_file
if release_values != {}: if release_values != {}:
fd, path = tempfile.mkstemp(suffix='.yml') fd, path = tempfile.mkstemp(suffix=".yml")
with open(path, 'w') as yaml_file: with open(path, "w") as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False) yaml.dump(release_values, yaml_file, default_flow_style=False)
deploy_command += " -f=" + path deploy_command += " -f=" + path
@@ -434,8 +450,7 @@ def deploy(command, release_name, release_values, chart_name, wait,
return deploy_command return deploy_command
def delete(command, release_name, purge, disable_hook, def delete(command, release_name, purge, disable_hook, wait, wait_timeout):
wait, wait_timeout):
""" """
Delete release chart Delete release chart
""" """
@@ -462,7 +477,7 @@ def delete(command, release_name, purge, disable_hook,
def load_values_files(values_files): def load_values_files(values_files):
values = {} values = {}
for values_file in values_files or []: for values_file in values_files or []:
with open(values_file, 'r') as fd: with open(values_file, "r") as fd:
content = yaml.safe_load(fd) content = yaml.safe_load(fd)
if not isinstance(content, dict): if not isinstance(content, dict):
continue continue
@@ -489,8 +504,16 @@ def has_plugin(command, plugin):
return False return False
def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values, def helmdiff_check(
values_files=None, chart_version=None, replace=False): module,
helm_cmd,
release_name,
chart_ref,
release_values,
values_files=None,
chart_version=None,
replace=False,
):
""" """
Use helm diff to determine if a release would change by upgrading a chart. Use helm diff to determine if a release would change by upgrading a chart.
""" """
@@ -504,8 +527,8 @@ def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values,
cmd += " " + "--reset-values" cmd += " " + "--reset-values"
if release_values != {}: if release_values != {}:
fd, path = tempfile.mkstemp(suffix='.yml') fd, path = tempfile.mkstemp(suffix=".yml")
with open(path, 'w') as yaml_file: with open(path, "w") as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False) yaml.dump(release_values, yaml_file, default_flow_style=False)
cmd += " -f=" + path cmd += " -f=" + path
@@ -522,60 +545,83 @@ def default_check(release_status, chart_info, values=None, values_files=None):
Use default check to determine if release would change by upgrading a chart. Use default check to determine if release would change by upgrading a chart.
""" """
# the 'appVersion' specification is optional in a chart # the 'appVersion' specification is optional in a chart
chart_app_version = chart_info.get('appVersion', None) chart_app_version = chart_info.get("appVersion", None)
released_app_version = release_status.get('app_version', None) released_app_version = release_status.get("app_version", None)
# when deployed without an 'appVersion' chart value the 'helm list' command will return the entry `app_version: ""` # when deployed without an 'appVersion' chart value the 'helm list' command will return the entry `app_version: ""`
appversion_is_same = (chart_app_version == released_app_version) or (chart_app_version is None and released_app_version == "") appversion_is_same = (chart_app_version == released_app_version) or (
chart_app_version is None and released_app_version == ""
)
if values_files: if values_files:
values_match = release_status['values'] == load_values_files(values_files) values_match = release_status["values"] == load_values_files(values_files)
else: else:
values_match = release_status['values'] == values values_match = release_status["values"] == values
return not values_match \ return (
or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"] \ not values_match
or (chart_info["name"] + "-" + chart_info["version"]) != release_status["chart"]
or not appversion_is_same or not appversion_is_same
)
def main(): def main():
global module global module
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
chart_ref=dict(type='path'), chart_ref=dict(type="path"),
chart_repo_url=dict(type='str'), chart_repo_url=dict(type="str"),
chart_version=dict(type='str'), chart_version=dict(type="str"),
release_name=dict(type='str', required=True, aliases=['name']), release_name=dict(type="str", required=True, aliases=["name"]),
release_namespace=dict(type='str', required=True, aliases=['namespace']), release_namespace=dict(type="str", required=True, aliases=["namespace"]),
release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), release_state=dict(
release_values=dict(type='dict', default={}, aliases=['values']), default="present", choices=["present", "absent"], aliases=["state"]
values_files=dict(type='list', default=[], elements='str'), ),
update_repo_cache=dict(type='bool', default=False), release_values=dict(type="dict", default={}, aliases=["values"]),
values_files=dict(type="list", default=[], elements="str"),
update_repo_cache=dict(type="bool", default=False),
# Helm options # Helm options
disable_hook=dict(type='bool', default=False), disable_hook=dict(type="bool", default=False),
force=dict(type='bool', default=False), force=dict(type="bool", default=False),
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])), context=dict(
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])), type="str",
purge=dict(type='bool', default=True), aliases=["kube_context"],
wait=dict(type='bool', default=False), fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]),
wait_timeout=dict(type='str'), ),
timeout=dict(type='str'), kubeconfig=dict(
atomic=dict(type='bool', default=False), type="path",
create_namespace=dict(type='bool', default=False), aliases=["kubeconfig_path"],
replace=dict(type='bool', default=False), fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
skip_crds=dict(type='bool', default=False), ),
history_max=dict(type='int'), purge=dict(type="bool", default=True),
wait=dict(type="bool", default=False),
wait_timeout=dict(type="str"),
timeout=dict(type="str"),
atomic=dict(type="bool", default=False),
create_namespace=dict(type="bool", default=False),
replace=dict(type="bool", default=False),
skip_crds=dict(type="bool", default=False),
history_max=dict(type="int"),
# Generic auth key # Generic auth key
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])), host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])), ca_cert=dict(
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])), type="path",
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY'])) aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
),
validate_certs=dict(
type="bool",
default=True,
aliases=["verify_ssl"],
fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]),
),
api_key=dict(
type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"])
),
), ),
required_if=[ required_if=[
('release_state', 'present', ['release_name', 'chart_ref']), ("release_state", "present", ["release_name", "chart_ref"]),
('release_state', 'absent', ['release_name']) ("release_state", "absent", ["release_name"]),
], ],
mutually_exclusive=[ mutually_exclusive=[
("context", "ca_cert"), ("context", "ca_cert"),
@@ -591,33 +637,33 @@ def main():
changed = False changed = False
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
chart_ref = module.params.get('chart_ref') chart_ref = module.params.get("chart_ref")
chart_repo_url = module.params.get('chart_repo_url') chart_repo_url = module.params.get("chart_repo_url")
chart_version = module.params.get('chart_version') chart_version = module.params.get("chart_version")
release_name = module.params.get('release_name') release_name = module.params.get("release_name")
release_state = module.params.get('release_state') release_state = module.params.get("release_state")
release_values = module.params.get('release_values') release_values = module.params.get("release_values")
values_files = module.params.get('values_files') values_files = module.params.get("values_files")
update_repo_cache = module.params.get('update_repo_cache') update_repo_cache = module.params.get("update_repo_cache")
# Helm options # Helm options
disable_hook = module.params.get('disable_hook') disable_hook = module.params.get("disable_hook")
force = module.params.get('force') force = module.params.get("force")
purge = module.params.get('purge') purge = module.params.get("purge")
wait = module.params.get('wait') wait = module.params.get("wait")
wait_timeout = module.params.get('wait_timeout') wait_timeout = module.params.get("wait_timeout")
atomic = module.params.get('atomic') atomic = module.params.get("atomic")
create_namespace = module.params.get('create_namespace') create_namespace = module.params.get("create_namespace")
replace = module.params.get('replace') replace = module.params.get("replace")
skip_crds = module.params.get('skip_crds') skip_crds = module.params.get("skip_crds")
history_max = module.params.get('history_max') history_max = module.params.get("history_max")
timeout = module.params.get('timeout') timeout = module.params.get("timeout")
if bin_path is not None: if bin_path is not None:
helm_cmd_common = bin_path helm_cmd_common = bin_path
else: else:
helm_cmd_common = module.get_bin_path('helm', required=True) helm_cmd_common = module.get_bin_path("helm", required=True)
if update_repo_cache: if update_repo_cache:
run_repo_update(module, helm_cmd_common) run_repo_update(module, helm_cmd_common)
@@ -635,11 +681,15 @@ def main():
if wait: if wait:
helm_version = get_helm_version(module, helm_cmd_common) helm_version = get_helm_version(module, helm_cmd_common)
if LooseVersion(helm_version) < LooseVersion("3.7.0"): if LooseVersion(helm_version) < LooseVersion("3.7.0"):
opt_result['warnings'] = [] opt_result["warnings"] = []
opt_result['warnings'].append("helm uninstall support option --wait for helm release >= 3.7.0") opt_result["warnings"].append(
"helm uninstall support option --wait for helm release >= 3.7.0"
)
wait = False wait = False
helm_cmd = delete(helm_cmd, release_name, purge, disable_hook, wait, wait_timeout) helm_cmd = delete(
helm_cmd, release_name, purge, disable_hook, wait, wait_timeout
)
changed = True changed = True
elif release_state == "present": elif release_state == "present":
@@ -653,54 +703,87 @@ def main():
chart_info = fetch_chart_info(module, helm_cmd, chart_ref) chart_info = fetch_chart_info(module, helm_cmd, chart_ref)
if release_status is None: # Not installed if release_status is None: # Not installed
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, helm_cmd = deploy(
disable_hook, False, values_files=values_files, atomic=atomic, helm_cmd,
create_namespace=create_namespace, replace=replace, release_name,
skip_crds=skip_crds, history_max=history_max, timeout=timeout) release_values,
chart_ref,
wait,
wait_timeout,
disable_hook,
False,
values_files=values_files,
atomic=atomic,
create_namespace=create_namespace,
replace=replace,
skip_crds=skip_crds,
history_max=history_max,
timeout=timeout,
)
changed = True changed = True
else: else:
if has_plugin(helm_cmd_common, "diff") and not chart_repo_url: if has_plugin(helm_cmd_common, "diff") and not chart_repo_url:
would_change = helmdiff_check(module, helm_cmd_common, release_name, chart_ref, would_change = helmdiff_check(
release_values, values_files, chart_version, replace) module,
helm_cmd_common,
release_name,
chart_ref,
release_values,
values_files,
chart_version,
replace,
)
else: else:
module.warn("The default idempotency check can fail to report changes in certain cases. " module.warn(
"Install helm diff for better results.") "The default idempotency check can fail to report changes in certain cases. "
would_change = default_check(release_status, chart_info, release_values, values_files) "Install helm diff for better results."
)
would_change = default_check(
release_status, chart_info, release_values, values_files
)
if force or would_change: if force or would_change:
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, helm_cmd = deploy(
disable_hook, force, values_files=values_files, atomic=atomic, helm_cmd,
create_namespace=create_namespace, replace=replace, release_name,
skip_crds=skip_crds, history_max=history_max, timeout=timeout) release_values,
chart_ref,
wait,
wait_timeout,
disable_hook,
force,
values_files=values_files,
atomic=atomic,
create_namespace=create_namespace,
replace=replace,
skip_crds=skip_crds,
history_max=history_max,
timeout=timeout,
)
changed = True changed = True
if module.check_mode: if module.check_mode:
check_status = { check_status = {"values": {"current": {}, "declared": {}}}
'values': {
"current": {},
"declared": {},
}
}
if release_status: if release_status:
check_status['values']['current'] = release_status['values'] check_status["values"]["current"] = release_status["values"]
check_status['values']['declared'] = release_status check_status["values"]["declared"] = release_status
module.exit_json( module.exit_json(
changed=changed, changed=changed,
command=helm_cmd, command=helm_cmd,
status=check_status, status=check_status,
stdout='', stdout="",
stderr='', stderr="",
**opt_result, **opt_result,
) )
elif not changed: elif not changed:
module.exit_json( module.exit_json(
changed=False, changed=False,
status=release_status, status=release_status,
stdout='', stdout="",
stderr='', stderr="",
command=helm_cmd, command=helm_cmd,
**opt_result, **opt_result,
) )
@@ -717,5 +800,5 @@ def main():
) )
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -4,10 +4,11 @@
# Copyright: (c) 2020, Ansible Project # Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
--- ---
module: helm_info module: helm_info
@@ -40,16 +41,16 @@ options:
aliases: [ namespace ] aliases: [ namespace ]
extends_documentation_fragment: extends_documentation_fragment:
- kubernetes.core.helm_common_options - kubernetes.core.helm_common_options
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Deploy latest version of Grafana chart inside monitoring namespace - name: Deploy latest version of Grafana chart inside monitoring namespace
kubernetes.core.helm_info: kubernetes.core.helm_info:
name: test name: test
release_namespace: monitoring release_namespace: monitoring
''' """
RETURN = r''' RETURN = r"""
status: status:
type: complex type: complex
description: A dictionary of status output description: A dictionary of status output
@@ -87,26 +88,30 @@ status:
type: str type: str
returned: always returned: always
description: Dict of Values used to deploy description: Dict of Values used to deploy
''' """
import traceback import traceback
try: try:
import yaml import yaml
IMP_YAML = True IMP_YAML = True
except ImportError: except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm, get_values from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm,
get_values,
)
# Get Release from all deployed releases # Get Release from all deployed releases
def get_release(state, release_name): def get_release(state, release_name):
if state is not None: if state is not None:
for release in state: for release in state:
if release['name'] == release_name: if release["name"] == release_name:
return release return release
return None return None
@@ -119,8 +124,10 @@ def get_release_status(module, command, release_name):
if rc != 0: if rc != 0:
module.fail_json( module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
command=list_command rc, out, err
),
command=list_command,
) )
release = get_release(yaml.safe_load(out), release_name) release = get_release(yaml.safe_load(out), release_name)
@@ -128,7 +135,7 @@ def get_release_status(module, command, release_name):
if release is None: # not install if release is None: # not install
return None return None
release['values'] = get_values(module, command, release_name) release["values"] = get_values(module, command, release_name)
return release return release
@@ -138,25 +145,42 @@ def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
release_name=dict(type='str', required=True, aliases=['name']), release_name=dict(type="str", required=True, aliases=["name"]),
release_namespace=dict(type='str', required=True, aliases=['namespace']), release_namespace=dict(type="str", required=True, aliases=["namespace"]),
# Helm options # Helm options
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])), context=dict(
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])), type="str",
aliases=["kube_context"],
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]),
),
kubeconfig=dict(
type="path",
aliases=["kubeconfig_path"],
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
),
# Generic auth key # Generic auth key
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])), host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])), ca_cert=dict(
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])), type="path",
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY'])) aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
),
validate_certs=dict(
type="bool",
default=True,
aliases=["verify_ssl"],
fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]),
),
api_key=dict(
type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"])
),
), ),
mutually_exclusive=[ mutually_exclusive=[
("context", "ca_cert"), ("context", "ca_cert"),
("context", "validate_certs"), ("context", "validate_certs"),
("kubeconfig", "ca_cert"), ("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs") ("kubeconfig", "validate_certs"),
], ],
supports_check_mode=True, supports_check_mode=True,
) )
@@ -164,13 +188,13 @@ def main():
if not IMP_YAML: if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
release_name = module.params.get('release_name') release_name = module.params.get("release_name")
if bin_path is not None: if bin_path is not None:
helm_cmd_common = bin_path helm_cmd_common = bin_path
else: else:
helm_cmd_common = module.get_bin_path('helm', required=True) helm_cmd_common = module.get_bin_path("helm", required=True)
release_status = get_release_status(module, helm_cmd_common, release_name) release_status = get_release_status(module, helm_cmd_common, release_name)
@@ -180,5 +204,5 @@ def main():
module.exit_json(changed=False) module.exit_json(changed=False)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
--- ---
module: helm_plugin module: helm_plugin
short_description: Manage Helm plugins short_description: Manage Helm plugins
@@ -50,9 +50,9 @@ options:
version_added: "2.3.0" version_added: "2.3.0"
extends_documentation_fragment: extends_documentation_fragment:
- kubernetes.core.helm_common_options - kubernetes.core.helm_common_options
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Install Helm env plugin - name: Install Helm env plugin
kubernetes.core.helm_plugin: kubernetes.core.helm_plugin:
plugin_path: https://github.com/adamreese/helm-env plugin_path: https://github.com/adamreese/helm-env
@@ -78,9 +78,9 @@ EXAMPLES = r'''
kubernetes.core.helm_plugin: kubernetes.core.helm_plugin:
plugin_name: secrets plugin_name: secrets
state: latest state: latest
''' """
RETURN = r''' RETURN = r"""
stdout: stdout:
type: str type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log description: Full `helm` command stdout, in case you want to display it or examine the event log
@@ -106,33 +106,53 @@ rc:
description: Helm plugin command return code description: Helm plugin command return code
returned: always returned: always
sample: 1 sample: 1
''' """
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm, run_helm,
get_helm_plugin_list, get_helm_plugin_list,
parse_helm_plugin_list parse_helm_plugin_list,
) )
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
state=dict(type='str', default='present', choices=['present', 'absent', 'latest']), state=dict(
plugin_path=dict(type='str',), type="str", default="present", choices=["present", "absent", "latest"]
plugin_name=dict(type='str',), ),
plugin_version=dict(type='str',), plugin_path=dict(type="str",),
plugin_name=dict(type="str",),
plugin_version=dict(type="str",),
# Helm options # Helm options
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])), context=dict(
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])), type="str",
aliases=["kube_context"],
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]),
),
kubeconfig=dict(
type="path",
aliases=["kubeconfig_path"],
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
),
# Generic auth key # Generic auth key
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])), host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])), ca_cert=dict(
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])), type="path",
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY'])) aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
),
validate_certs=dict(
type="bool",
default=True,
aliases=["verify_ssl"],
fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]),
),
api_key=dict(
type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"])
),
), ),
supports_check_mode=True, supports_check_mode=True,
required_if=[ required_if=[
@@ -141,37 +161,37 @@ def main():
("state", "latest", ("plugin_name",)), ("state", "latest", ("plugin_name",)),
], ],
mutually_exclusive=[ mutually_exclusive=[
('plugin_name', 'plugin_path'), ("plugin_name", "plugin_path"),
("context", "ca_cert"), ("context", "ca_cert"),
("context", "validate_certs"), ("context", "validate_certs"),
("kubeconfig", "ca_cert"), ("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs") ("kubeconfig", "validate_certs"),
], ],
) )
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
state = module.params.get('state') state = module.params.get("state")
if bin_path is not None: if bin_path is not None:
helm_cmd_common = bin_path helm_cmd_common = bin_path
else: else:
helm_cmd_common = 'helm' helm_cmd_common = "helm"
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True) helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
helm_cmd_common += " plugin" helm_cmd_common += " plugin"
if state == 'present': if state == "present":
helm_cmd_common += " install %s" % module.params.get('plugin_path') helm_cmd_common += " install %s" % module.params.get("plugin_path")
plugin_version = module.params.get('plugin_version') plugin_version = module.params.get("plugin_version")
if plugin_version is not None: if plugin_version is not None:
helm_cmd_common += " --version=%s" % plugin_version helm_cmd_common += " --version=%s" % plugin_version
if not module.check_mode: if not module.check_mode:
rc, out, err = run_helm(module, helm_cmd_common, fails_on_error=False) rc, out, err = run_helm(module, helm_cmd_common, fails_on_error=False)
else: else:
rc, out, err = (0, '', '') rc, out, err = (0, "", "")
if rc == 1 and 'plugin already exists' in err: if rc == 1 and "plugin already exists" in err:
module.exit_json( module.exit_json(
failed=False, failed=False,
changed=False, changed=False,
@@ -179,7 +199,7 @@ def main():
command=helm_cmd_common, command=helm_cmd_common,
stdout=out, stdout=out,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
elif rc == 0: elif rc == 0:
module.exit_json( module.exit_json(
@@ -199,8 +219,8 @@ def main():
stderr=err, stderr=err,
rc=rc, rc=rc,
) )
elif state == 'absent': elif state == "absent":
plugin_name = module.params.get('plugin_name') plugin_name = module.params.get("plugin_name")
rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common) rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common)
out = parse_helm_plugin_list(module, output=output.splitlines()) out = parse_helm_plugin_list(module, output=output.splitlines())
@@ -212,7 +232,7 @@ def main():
command=helm_cmd_common + " list", command=helm_cmd_common + " list",
stdout=output, stdout=output,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
found = False found = False
@@ -228,14 +248,14 @@ def main():
command=helm_cmd_common + " list", command=helm_cmd_common + " list",
stdout=output, stdout=output,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
helm_uninstall_cmd = "%s uninstall %s" % (helm_cmd_common, plugin_name) helm_uninstall_cmd = "%s uninstall %s" % (helm_cmd_common, plugin_name)
if not module.check_mode: if not module.check_mode:
rc, out, err = run_helm(module, helm_uninstall_cmd, fails_on_error=False) rc, out, err = run_helm(module, helm_uninstall_cmd, fails_on_error=False)
else: else:
rc, out, err = (0, '', '') rc, out, err = (0, "", "")
if rc == 0: if rc == 0:
module.exit_json( module.exit_json(
@@ -244,7 +264,7 @@ def main():
command=helm_uninstall_cmd, command=helm_uninstall_cmd,
stdout=out, stdout=out,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
module.fail_json( module.fail_json(
msg="Failed to get Helm plugin uninstall", msg="Failed to get Helm plugin uninstall",
@@ -253,8 +273,8 @@ def main():
stderr=err, stderr=err,
rc=rc, rc=rc,
) )
elif state == 'latest': elif state == "latest":
plugin_name = module.params.get('plugin_name') plugin_name = module.params.get("plugin_name")
rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common) rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common)
out = parse_helm_plugin_list(module, output=output.splitlines()) out = parse_helm_plugin_list(module, output=output.splitlines())
@@ -266,7 +286,7 @@ def main():
command=helm_cmd_common + " list", command=helm_cmd_common + " list",
stdout=output, stdout=output,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
found = False found = False
@@ -282,14 +302,14 @@ def main():
command=helm_cmd_common + " list", command=helm_cmd_common + " list",
stdout=output, stdout=output,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
helm_update_cmd = "%s update %s" % (helm_cmd_common, plugin_name) helm_update_cmd = "%s update %s" % (helm_cmd_common, plugin_name)
if not module.check_mode: if not module.check_mode:
rc, out, err = run_helm(module, helm_update_cmd, fails_on_error=False) rc, out, err = run_helm(module, helm_update_cmd, fails_on_error=False)
else: else:
rc, out, err = (0, '', '') rc, out, err = (0, "", "")
if rc == 0: if rc == 0:
module.exit_json( module.exit_json(
@@ -298,7 +318,7 @@ def main():
command=helm_update_cmd, command=helm_update_cmd,
stdout=out, stdout=out,
stderr=err, stderr=err,
rc=rc rc=rc,
) )
module.fail_json( module.fail_json(
msg="Failed to get Helm plugin update", msg="Failed to get Helm plugin update",
@@ -309,5 +329,5 @@ def main():
) )
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
--- ---
module: helm_plugin_info module: helm_plugin_info
short_description: Gather information about Helm plugins short_description: Gather information about Helm plugins
@@ -27,18 +27,18 @@ options:
type: str type: str
extends_documentation_fragment: extends_documentation_fragment:
- kubernetes.core.helm_common_options - kubernetes.core.helm_common_options
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Gather Helm plugin info - name: Gather Helm plugin info
kubernetes.core.helm_plugin_info: kubernetes.core.helm_plugin_info:
- name: Gather Helm env plugin info - name: Gather Helm env plugin info
kubernetes.core.helm_plugin_info: kubernetes.core.helm_plugin_info:
plugin_name: env plugin_name: env
''' """
RETURN = r''' RETURN = r"""
stdout: stdout:
type: str type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log description: Full `helm` command stdout, in case you want to display it or examine the event log
@@ -68,7 +68,7 @@ rc:
description: Helm plugin command return code description: Helm plugin command return code
returned: always returned: always
sample: 1 sample: 1
''' """
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
@@ -80,39 +80,57 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
plugin_name=dict(type='str',), plugin_name=dict(type="str",),
# Helm options # Helm options
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])), context=dict(
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])), type="str",
aliases=["kube_context"],
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]),
),
kubeconfig=dict(
type="path",
aliases=["kubeconfig_path"],
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
),
# Generic auth key # Generic auth key
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])), host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])), ca_cert=dict(
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])), type="path",
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY'])) aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
),
validate_certs=dict(
type="bool",
default=True,
aliases=["verify_ssl"],
fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]),
),
api_key=dict(
type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"])
),
), ),
mutually_exclusive=[ mutually_exclusive=[
("context", "ca_cert"), ("context", "ca_cert"),
("context", "validate_certs"), ("context", "validate_certs"),
("kubeconfig", "ca_cert"), ("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs") ("kubeconfig", "validate_certs"),
], ],
supports_check_mode=True, supports_check_mode=True,
) )
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
if bin_path is not None: if bin_path is not None:
helm_cmd_common = bin_path helm_cmd_common = bin_path
else: else:
helm_cmd_common = 'helm' helm_cmd_common = "helm"
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True) helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
helm_cmd_common += " plugin" helm_cmd_common += " plugin"
plugin_name = module.params.get('plugin_name') plugin_name = module.params.get("plugin_name")
plugin_list = [] plugin_list = []
@@ -123,21 +141,13 @@ def main():
for line in out: for line in out:
if plugin_name is None: if plugin_name is None:
plugin_list.append( plugin_list.append(
{ {"name": line[0], "version": line[1], "description": line[2]}
"name": line[0],
"version": line[1],
"description": line[2],
}
) )
continue continue
if plugin_name == line[0]: if plugin_name == line[0]:
plugin_list.append( plugin_list.append(
{ {"name": line[0], "version": line[1], "description": line[2]}
"name": line[0],
"version": line[1],
"description": line[2],
}
) )
break break
@@ -151,5 +161,5 @@ def main():
) )
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -4,10 +4,11 @@
# Copyright: (c) 2020, Ansible Project # Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
--- ---
module: helm_repository module: helm_repository
@@ -64,9 +65,9 @@ options:
default: present default: present
aliases: [ state ] aliases: [ state ]
type: str type: str
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Add a repository - name: Add a repository
kubernetes.core.helm_repository: kubernetes.core.helm_repository:
name: stable name: stable
@@ -76,9 +77,9 @@ EXAMPLES = r'''
kubernetes.core.helm_repository: kubernetes.core.helm_repository:
name: redhat-charts name: redhat-charts
repo_url: https://redhat-developer.github.com/redhat-helm-charts repo_url: https://redhat-developer.github.com/redhat-helm-charts
''' """
RETURN = r''' RETURN = r"""
stdout: stdout:
type: str type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log description: Full `helm` command stdout, in case you want to display it or examine the event log
@@ -109,12 +110,13 @@ msg:
description: Error message returned by `helm` command description: Error message returned by `helm` command
returned: on failure returned: on failure
sample: 'Repository already have a repository named bitnami' sample: 'Repository already have a repository named bitnami'
''' """
import traceback import traceback
try: try:
import yaml import yaml
IMP_YAML = True IMP_YAML = True
except ImportError: except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
@@ -128,7 +130,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_he
def get_repository(state, repo_name): def get_repository(state, repo_name):
if state is not None: if state is not None:
for repository in state: for repository in state:
if repository['name'] == repo_name: if repository["name"] == repo_name:
return repository return repository
return None return None
@@ -144,15 +146,19 @@ def get_repository_status(module, command, repository_name):
return None return None
elif rc != 0: elif rc != 0:
module.fail_json( module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
command=list_command rc, out, err
),
command=list_command,
) )
return get_repository(yaml.safe_load(out), repository_name) return get_repository(yaml.safe_load(out), repository_name)
# Install repository # Install repository
def install_repository(command, repository_name, repository_url, repository_username, repository_password): def install_repository(
command, repository_name, repository_url, repository_username, repository_password
):
install_command = command + " repo add " + repository_name + " " + repository_url install_command = command + " repo add " + repository_name + " " + repository_url
if repository_username is not None and repository_password is not None: if repository_username is not None and repository_password is not None:
@@ -174,19 +180,17 @@ def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
repo_name=dict(type='str', aliases=['name'], required=True), repo_name=dict(type="str", aliases=["name"], required=True),
repo_url=dict(type='str', aliases=['url']), repo_url=dict(type="str", aliases=["url"]),
repo_username=dict(type='str', aliases=['username']), repo_username=dict(type="str", aliases=["username"]),
repo_password=dict(type='str', aliases=['password'], no_log=True), repo_password=dict(type="str", aliases=["password"], no_log=True),
repo_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), repo_state=dict(
default="present", choices=["present", "absent"], aliases=["state"]
),
), ),
required_together=[ required_together=[["repo_username", "repo_password"]],
['repo_username', 'repo_password'] required_if=[("repo_state", "present", ["repo_url"])],
],
required_if=[
('repo_state', 'present', ['repo_url']),
],
supports_check_mode=True, supports_check_mode=True,
) )
@@ -195,17 +199,17 @@ def main():
changed = False changed = False
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
repo_name = module.params.get('repo_name') repo_name = module.params.get("repo_name")
repo_url = module.params.get('repo_url') repo_url = module.params.get("repo_url")
repo_username = module.params.get('repo_username') repo_username = module.params.get("repo_username")
repo_password = module.params.get('repo_password') repo_password = module.params.get("repo_password")
repo_state = module.params.get('repo_state') repo_state = module.params.get("repo_state")
if bin_path is not None: if bin_path is not None:
helm_cmd = bin_path helm_cmd = bin_path
else: else:
helm_cmd = module.get_bin_path('helm', required=True) helm_cmd = module.get_bin_path("helm", required=True)
repository_status = get_repository_status(module, helm_cmd, repo_name) repository_status = get_repository_status(module, helm_cmd, repo_name)
@@ -214,10 +218,14 @@ def main():
changed = True changed = True
elif repo_state == "present": elif repo_state == "present":
if repository_status is None: if repository_status is None:
helm_cmd = install_repository(helm_cmd, repo_name, repo_url, repo_username, repo_password) helm_cmd = install_repository(
helm_cmd, repo_name, repo_url, repo_username, repo_password
)
changed = True changed = True
elif repository_status['url'] != repo_url: elif repository_status["url"] != repo_url:
module.fail_json(msg="Repository already have a repository named {0}".format(repo_name)) module.fail_json(
msg="Repository already have a repository named {0}".format(repo_name)
)
if module.check_mode: if module.check_mode:
module.exit_json(changed=changed) module.exit_json(changed=changed)
@@ -227,16 +235,18 @@ def main():
rc, out, err = run_helm(module, helm_cmd) rc, out, err = run_helm(module, helm_cmd)
if repo_password is not None: if repo_password is not None:
helm_cmd = helm_cmd.replace(repo_password, '******') helm_cmd = helm_cmd.replace(repo_password, "******")
if rc != 0: if rc != 0:
module.fail_json( module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
command=helm_cmd rc, out, err
),
command=helm_cmd,
) )
module.exit_json(changed=changed, stdout=out, stderr=err, command=helm_cmd) module.exit_json(changed=changed, stdout=out, stderr=err, command=helm_cmd)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: helm_template module: helm_template
@@ -79,9 +79,9 @@ options:
- Run C(helm repo update) before the operation. Can be run as part of the template generation or as a separate step. - Run C(helm repo update) before the operation. Can be run as part of the template generation or as a separate step.
default: false default: false
type: bool type: bool
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Render templates to specified directory - name: Render templates to specified directory
kubernetes.core.helm_template: kubernetes.core.helm_template:
chart_ref: stable/prometheus chart_ref: stable/prometheus
@@ -96,9 +96,9 @@ EXAMPLES = r'''
copy: copy:
dest: myfile.yaml dest: myfile.yaml
content: "{{ result.stdout }}" content: "{{ result.stdout }}"
''' """
RETURN = r''' RETURN = r"""
stdout: stdout:
type: str type: str
description: Full C(helm) command stdout. If no I(output_dir) has been provided this will contain the rendered templates as concatenated yaml documents. description: Full C(helm) command stdout. If no I(output_dir) has been provided this will contain the rendered templates as concatenated yaml documents.
@@ -114,13 +114,14 @@ command:
description: Full C(helm) command run by this module, in case you want to re-run the command outside the module or debug a problem. description: Full C(helm) command run by this module, in case you want to re-run the command outside the module or debug a problem.
returned: always returned: always
sample: helm template --output-dir mychart nginx-stable/nginx-ingress sample: helm template --output-dir mychart nginx-stable/nginx-ingress
''' """
import tempfile import tempfile
import traceback import traceback
try: try:
import yaml import yaml
IMP_YAML = True IMP_YAML = True
except ImportError: except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
@@ -130,8 +131,16 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm
def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir=None, def template(
release_values=None, values_files=None, include_crds=False): cmd,
chart_ref,
chart_repo_url=None,
chart_version=None,
output_dir=None,
release_values=None,
values_files=None,
include_crds=False,
):
cmd += " template " + chart_ref cmd += " template " + chart_ref
if chart_repo_url: if chart_repo_url:
@@ -144,8 +153,8 @@ def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir
cmd += " --output-dir=" + output_dir cmd += " --output-dir=" + output_dir
if release_values: if release_values:
fd, path = tempfile.mkstemp(suffix='.yml') fd, path = tempfile.mkstemp(suffix=".yml")
with open(path, 'w') as yaml_file: with open(path, "w") as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False) yaml.dump(release_values, yaml_file, default_flow_style=False)
cmd += " -f=" + path cmd += " -f=" + path
@@ -162,43 +171,49 @@ def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
binary_path=dict(type='path'), binary_path=dict(type="path"),
chart_ref=dict(type='path', required=True), chart_ref=dict(type="path", required=True),
chart_repo_url=dict(type='str'), chart_repo_url=dict(type="str"),
chart_version=dict(type='str'), chart_version=dict(type="str"),
include_crds=dict(type='bool', default=False), include_crds=dict(type="bool", default=False),
output_dir=dict(type='path'), output_dir=dict(type="path"),
release_values=dict(type='dict', default={}, aliases=['values']), release_values=dict(type="dict", default={}, aliases=["values"]),
values_files=dict(type='list', default=[], elements='str'), values_files=dict(type="list", default=[], elements="str"),
update_repo_cache=dict(type='bool', default=False) update_repo_cache=dict(type="bool", default=False),
), ),
supports_check_mode=True supports_check_mode=True,
) )
check_mode = module.check_mode check_mode = module.check_mode
bin_path = module.params.get('binary_path') bin_path = module.params.get("binary_path")
chart_ref = module.params.get('chart_ref') chart_ref = module.params.get("chart_ref")
chart_repo_url = module.params.get('chart_repo_url') chart_repo_url = module.params.get("chart_repo_url")
chart_version = module.params.get('chart_version') chart_version = module.params.get("chart_version")
include_crds = module.params.get('include_crds') include_crds = module.params.get("include_crds")
output_dir = module.params.get('output_dir') output_dir = module.params.get("output_dir")
release_values = module.params.get('release_values') release_values = module.params.get("release_values")
values_files = module.params.get('values_files') values_files = module.params.get("values_files")
update_repo_cache = module.params.get('update_repo_cache') update_repo_cache = module.params.get("update_repo_cache")
if not IMP_YAML: if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
helm_cmd = bin_path or module.get_bin_path('helm', required=True) helm_cmd = bin_path or module.get_bin_path("helm", required=True)
if update_repo_cache: if update_repo_cache:
update_cmd = helm_cmd + " repo update" update_cmd = helm_cmd + " repo update"
run_helm(module, update_cmd) run_helm(module, update_cmd)
tmpl_cmd = template(helm_cmd, chart_ref, chart_repo_url=chart_repo_url, tmpl_cmd = template(
chart_version=chart_version, output_dir=output_dir, helm_cmd,
release_values=release_values, values_files=values_files, chart_ref,
include_crds=include_crds) chart_repo_url=chart_repo_url,
chart_version=chart_version,
output_dir=output_dir,
release_values=release_values,
values_files=values_files,
include_crds=include_crds,
)
if not check_mode: if not check_mode:
rc, out, err = run_helm(module, tmpl_cmd) rc, out, err = run_helm(module, tmpl_cmd)
@@ -207,14 +222,9 @@ def main():
rc = 0 rc = 0
module.exit_json( module.exit_json(
failed=False, failed=False, changed=True, command=tmpl_cmd, stdout=out, stderr=err, rc=rc
changed=True,
command=tmpl_cmd,
stdout=out,
stderr=err,
rc=rc
) )
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s module: k8s
@@ -158,9 +158,9 @@ requirements:
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
- "jsonpatch" - "jsonpatch"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Create a k8s namespace - name: Create a k8s namespace
kubernetes.core.k8s: kubernetes.core.k8s:
name: testing name: testing
@@ -302,9 +302,9 @@ EXAMPLES = r'''
- name: py - name: py
image: python:3.7-alpine image: python:3.7-alpine
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- The created, patched, or otherwise present object. Will be empty in the case of a deletion. - The created, patched, or otherwise present object. Will be empty in the case of a deletion.
@@ -344,20 +344,27 @@ result:
description: error while trying to create/delete the object. description: error while trying to create/delete the object.
returned: error returned: error
type: complex type: complex
''' """
import copy import copy
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC) AUTH_ARG_SPEC,
WAIT_ARG_SPEC,
NAME_ARG_SPEC,
RESOURCE_ARG_SPEC,
DELETE_OPTS_ARG_SPEC,
)
def validate_spec(): def validate_spec():
return dict( return dict(
fail_on_error=dict(type='bool'), fail_on_error=dict(type="bool"),
version=dict(), version=dict(),
strict=dict(type='bool', default=True) strict=dict(type="bool", default=True),
) )
@@ -366,17 +373,23 @@ def argspec():
argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC))
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC)) argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC)) argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC))
argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) argument_spec["merge_type"] = dict(
argument_spec['validate'] = dict(type='dict', default=None, options=validate_spec()) type="list", elements="str", choices=["json", "merge", "strategic-merge"]
argument_spec['append_hash'] = dict(type='bool', default=False) )
argument_spec['apply'] = dict(type='bool', default=False) argument_spec["validate"] = dict(type="dict", default=None, options=validate_spec())
argument_spec['template'] = dict(type='raw', default=None) argument_spec["append_hash"] = dict(type="bool", default=False)
argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) argument_spec["apply"] = dict(type="bool", default=False)
argument_spec['continue_on_error'] = dict(type='bool', default=False) argument_spec["template"] = dict(type="raw", default=None)
argument_spec['state'] = dict(default='present', choices=['present', 'absent', 'patched']) argument_spec["delete_options"] = dict(
argument_spec['force'] = dict(type='bool', default=False) type="dict", default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)
argument_spec['label_selectors'] = dict(type='list', elements='str') )
argument_spec['generate_name'] = dict() argument_spec["continue_on_error"] = dict(type="bool", default=False)
argument_spec["state"] = dict(
default="present", choices=["present", "absent", "patched"]
)
argument_spec["force"] = dict(type="bool", default=False)
argument_spec["label_selectors"] = dict(type="list", elements="str")
argument_spec["generate_name"] = dict()
return argument_spec return argument_spec
@@ -392,11 +405,11 @@ def execute_module(module, k8s_ansible_mixin):
k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn
k8s_ansible_mixin.warnings = [] k8s_ansible_mixin.warnings = []
k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get('kind') k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get("kind")
k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get('api_version') k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get("api_version")
k8s_ansible_mixin.name = k8s_ansible_mixin.params.get('name') k8s_ansible_mixin.name = k8s_ansible_mixin.params.get("name")
k8s_ansible_mixin.generate_name = k8s_ansible_mixin.params.get('generate_name') k8s_ansible_mixin.generate_name = k8s_ansible_mixin.params.get("generate_name")
k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get('namespace') k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get("namespace")
k8s_ansible_mixin.check_library_version() k8s_ansible_mixin.check_library_version()
k8s_ansible_mixin.set_resource_definitions(module) k8s_ansible_mixin.set_resource_definitions(module)
@@ -405,20 +418,26 @@ def execute_module(module, k8s_ansible_mixin):
def main(): def main():
mutually_exclusive = [ mutually_exclusive = [
('resource_definition', 'src'), ("resource_definition", "src"),
('merge_type', 'apply'), ("merge_type", "apply"),
('template', 'resource_definition'), ("template", "resource_definition"),
('template', 'src'), ("template", "src"),
('name', 'generate_name'), ("name", "generate_name"),
] ]
module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True) module = AnsibleModule(
argument_spec=argspec(),
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -4,10 +4,11 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_cluster_info module: k8s_cluster_info
version_added: "0.11.1" version_added: "0.11.1"
@@ -36,9 +37,9 @@ requirements:
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Get Cluster information - name: Get Cluster information
kubernetes.core.k8s_cluster_info: kubernetes.core.k8s_cluster_info:
register: api_status register: api_status
@@ -47,9 +48,9 @@ EXAMPLES = r'''
kubernetes.core.k8s_cluster_info: kubernetes.core.k8s_cluster_info:
invalidate_cache: False invalidate_cache: False
register: api_status register: api_status
''' """
RETURN = r''' RETURN = r"""
connection: connection:
description: description:
- Connection information - Connection information
@@ -136,7 +137,7 @@ apis:
description: Resource singular name description: Resource singular name
returned: success returned: success
type: str type: str
''' """
import copy import copy
@@ -145,7 +146,10 @@ from collections import defaultdict
HAS_K8S = False HAS_K8S = False
try: try:
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
ResourceList,
)
HAS_K8S = True HAS_K8S = True
except ImportError as e: except ImportError as e:
K8S_IMP_ERR = e K8S_IMP_ERR = e
@@ -154,12 +158,18 @@ except ImportError as e:
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC) AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
)
def execute_module(module, client): def execute_module(module, client):
invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False) invalidate_cache = boolean(
module.params.get("invalidate_cache", True), strict=False
)
if invalidate_cache: if invalidate_cache:
client.resources.invalidate_cache() client.resources.invalidate_cache()
results = defaultdict(dict) results = defaultdict(dict)
@@ -167,47 +177,60 @@ def execute_module(module, client):
resource = resource[0] resource = resource[0]
if isinstance(resource, ResourceList): if isinstance(resource, ResourceList):
continue continue
key = resource.group_version if resource.group == '' else '/'.join([resource.group, resource.group_version.split('/')[-1]]) key = (
resource.group_version
if resource.group == ""
else "/".join([resource.group, resource.group_version.split("/")[-1]])
)
results[key][resource.kind] = { results[key][resource.kind] = {
'categories': resource.categories if resource.categories else [], "categories": resource.categories if resource.categories else [],
'name': resource.name, "name": resource.name,
'namespaced': resource.namespaced, "namespaced": resource.namespaced,
'preferred': resource.preferred, "preferred": resource.preferred,
'short_names': resource.short_names if resource.short_names else [], "short_names": resource.short_names if resource.short_names else [],
'singular_name': resource.singular_name, "singular_name": resource.singular_name,
} }
configuration = client.configuration configuration = client.configuration
connection = { connection = {
'cert_file': configuration.cert_file, "cert_file": configuration.cert_file,
'host': configuration.host, "host": configuration.host,
'password': configuration.password, "password": configuration.password,
'proxy': configuration.proxy, "proxy": configuration.proxy,
'ssl_ca_cert': configuration.ssl_ca_cert, "ssl_ca_cert": configuration.ssl_ca_cert,
'username': configuration.username, "username": configuration.username,
'verify_ssl': configuration.verify_ssl, "verify_ssl": configuration.verify_ssl,
} }
from kubernetes import __version__ as version from kubernetes import __version__ as version
version_info = { version_info = {
'client': version, "client": version,
'server': client.version, "server": client.version,
} }
module.exit_json(changed=False, apis=results, connection=connection, version=version_info) module.exit_json(
changed=False, apis=results, connection=connection, version=version_info
)
def argspec(): def argspec():
spec = copy.deepcopy(AUTH_ARG_SPEC) spec = copy.deepcopy(AUTH_ARG_SPEC)
spec['invalidate_cache'] = dict(type='bool', default=True) spec["invalidate_cache"] = dict(type="bool", default=True)
return spec return spec
def main(): def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
if not HAS_K8S: if not HAS_K8S:
module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_EXC, module.fail_json(
error=to_native(K8S_IMP_ERR)) msg=missing_required_lib("kubernetes"),
from ansible_collections.kubernetes.core.plugins.module_utils.common import get_api_client exception=K8S_IMP_EXC,
error=to_native(K8S_IMP_ERR),
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
get_api_client,
)
execute_module(module, client=get_api_client(module=module)) execute_module(module, client=get_api_client(module=module))
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -4,10 +4,11 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_cp module: k8s_cp
@@ -78,9 +79,9 @@ options:
notes: notes:
- the tar binary is required on the container when copying from local filesystem to pod. - the tar binary is required on the container when copying from local filesystem to pod.
''' """
EXAMPLES = r''' EXAMPLES = r"""
# kubectl cp /tmp/foo some-namespace/some-pod:/tmp/bar # kubectl cp /tmp/foo some-namespace/some-pod:/tmp/bar
- name: Copy /tmp/foo local file to /tmp/bar in a remote pod - name: Copy /tmp/foo local file to /tmp/bar in a remote pod
kubernetes.core.k8s_cp: kubernetes.core.k8s_cp:
@@ -125,16 +126,16 @@ EXAMPLES = r'''
pod: some-pod pod: some-pod
remote_path: /tmp/foo.txt remote_path: /tmp/foo.txt
content: "This content will be copied into remote file" content: "This content will be copied into remote file"
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- message describing the copy operation successfully done. - message describing the copy operation successfully done.
returned: success returned: success
type: str type: str
''' """
import copy import copy
import os import os
@@ -146,13 +147,23 @@ import tarfile
# from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule # from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, get_api_client from ansible_collections.kubernetes.core.plugins.module_utils.common import (
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC K8sAnsibleMixin,
get_api_client,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
)
try: try:
from kubernetes.client.api import core_v1_api from kubernetes.client.api import core_v1_api
from kubernetes.stream import stream from kubernetes.stream import stream
from kubernetes.stream.ws_client import STDOUT_CHANNEL, STDERR_CHANNEL, ERROR_CHANNEL, ABNF from kubernetes.stream.ws_client import (
STDOUT_CHANNEL,
STDERR_CHANNEL,
ERROR_CHANNEL,
ABNF,
)
except ImportError: except ImportError:
pass pass
@@ -164,22 +175,21 @@ except ImportError:
class K8SCopy(metaclass=ABCMeta): class K8SCopy(metaclass=ABCMeta):
def __init__(self, module, client): def __init__(self, module, client):
self.client = client self.client = client
self.module = module self.module = module
self.api_instance = core_v1_api.CoreV1Api(client.client) self.api_instance = core_v1_api.CoreV1Api(client.client)
self.local_path = module.params.get('local_path') self.local_path = module.params.get("local_path")
self.name = module.params.get('pod') self.name = module.params.get("pod")
self.namespace = module.params.get('namespace') self.namespace = module.params.get("namespace")
self.remote_path = module.params.get('remote_path') self.remote_path = module.params.get("remote_path")
self.content = module.params.get('content') self.content = module.params.get("content")
self.no_preserve = module.params.get('no_preserve') self.no_preserve = module.params.get("no_preserve")
self.container_arg = {} self.container_arg = {}
if module.params.get('container'): if module.params.get("container"):
self.container_arg['container'] = module.params.get('container') self.container_arg["container"] = module.params.get("container")
@abstractmethod @abstractmethod
def run(self): def run(self):
@@ -190,6 +200,7 @@ class K8SCopyFromPod(K8SCopy):
""" """
Copy files/directory from Pod into local filesystem Copy files/directory from Pod into local filesystem
""" """
def __init__(self, module, client): def __init__(self, module, client):
super(K8SCopyFromPod, self).__init__(module, client) super(K8SCopyFromPod, self).__init__(module, client)
self.is_remote_path_dir = None self.is_remote_path_dir = None
@@ -201,31 +212,48 @@ class K8SCopyFromPod(K8SCopy):
if it is a directory the file list will be updated accordingly if it is a directory the file list will be updated accordingly
""" """
try: try:
find_cmd = ['find', self.remote_path, '-type', 'f', '-name', '*'] find_cmd = ["find", self.remote_path, "-type", "f", "-name", "*"]
response = stream(self.api_instance.connect_get_namespaced_pod_exec, response = stream(
self.name, self.api_instance.connect_get_namespaced_pod_exec,
self.namespace, self.name,
command=find_cmd, self.namespace,
stdout=True, stderr=True, command=find_cmd,
stdin=False, tty=False, stdout=True,
_preload_content=False, **self.container_arg) stderr=True,
stdin=False,
tty=False,
_preload_content=False,
**self.container_arg
)
except Exception as e: except Exception as e:
self.module.fail_json(msg="Failed to execute on pod {0}/{1} due to : {2}".format(self.namespace, self.name, to_native(e))) self.module.fail_json(
msg="Failed to execute on pod {0}/{1} due to : {2}".format(
self.namespace, self.name, to_native(e)
)
)
stderr = [] stderr = []
while response.is_open(): while response.is_open():
response.update(timeout=1) response.update(timeout=1)
if response.peek_stdout(): if response.peek_stdout():
self.files_to_copy.extend(response.read_stdout().rstrip('\n').split('\n')) self.files_to_copy.extend(
response.read_stdout().rstrip("\n").split("\n")
)
if response.peek_stderr(): if response.peek_stderr():
err = response.read_stderr() err = response.read_stderr()
if "No such file or directory" in err: if "No such file or directory" in err:
self.module.fail_json(msg="{0} does not exist in remote pod filesystem".format(self.remote_path)) self.module.fail_json(
msg="{0} does not exist in remote pod filesystem".format(
self.remote_path
)
)
stderr.append(err) stderr.append(err)
error = response.read_channel(ERROR_CHANNEL) error = response.read_channel(ERROR_CHANNEL)
response.close() response.close()
error = yaml.safe_load(error) error = yaml.safe_load(error)
if error['status'] != 'Success': if error["status"] != "Success":
self.module.fail_json(msg="Failed to execute on Pod due to: {0}".format(error)) self.module.fail_json(
msg="Failed to execute on Pod due to: {0}".format(error)
)
def read(self): def read(self):
self.stdout = None self.stdout = None
@@ -235,12 +263,15 @@ class K8SCopyFromPod(K8SCopy):
if not self.response.sock.connected: if not self.response.sock.connected:
self.response._connected = False self.response._connected = False
else: else:
ret, out, err = select((self.response.sock.sock, ), (), (), 0) ret, out, err = select((self.response.sock.sock,), (), (), 0)
if ret: if ret:
code, frame = self.response.sock.recv_data_frame(True) code, frame = self.response.sock.recv_data_frame(True)
if code == ABNF.OPCODE_CLOSE: if code == ABNF.OPCODE_CLOSE:
self.response._connected = False self.response._connected = False
elif code in (ABNF.OPCODE_BINARY, ABNF.OPCODE_TEXT) and len(frame.data) > 1: elif (
code in (ABNF.OPCODE_BINARY, ABNF.OPCODE_TEXT)
and len(frame.data) > 1
):
channel = frame.data[0] channel = frame.data[0]
content = frame.data[1:] content = frame.data[1:]
if content: if content:
@@ -250,7 +281,9 @@ class K8SCopyFromPod(K8SCopy):
self.stderr = content.decode("utf-8", "replace") self.stderr = content.decode("utf-8", "replace")
def copy(self): def copy(self):
is_remote_path_dir = len(self.files_to_copy) > 1 or self.files_to_copy[0] != self.remote_path is_remote_path_dir = (
len(self.files_to_copy) > 1 or self.files_to_copy[0] != self.remote_path
)
relpath_start = self.remote_path relpath_start = self.remote_path
if is_remote_path_dir and os.path.isdir(self.local_path): if is_remote_path_dir and os.path.isdir(self.local_path):
relpath_start = os.path.dirname(self.remote_path) relpath_start = os.path.dirname(self.remote_path)
@@ -258,20 +291,27 @@ class K8SCopyFromPod(K8SCopy):
for remote_file in self.files_to_copy: for remote_file in self.files_to_copy:
dest_file = self.local_path dest_file = self.local_path
if is_remote_path_dir: if is_remote_path_dir:
dest_file = os.path.join(self.local_path, os.path.relpath(remote_file, start=relpath_start)) dest_file = os.path.join(
self.local_path, os.path.relpath(remote_file, start=relpath_start)
)
# create directory to copy file in # create directory to copy file in
os.makedirs(os.path.dirname(dest_file), exist_ok=True) os.makedirs(os.path.dirname(dest_file), exist_ok=True)
pod_command = ['cat', remote_file] pod_command = ["cat", remote_file]
self.response = stream(self.api_instance.connect_get_namespaced_pod_exec, self.response = stream(
self.name, self.api_instance.connect_get_namespaced_pod_exec,
self.namespace, self.name,
command=pod_command, self.namespace,
stderr=True, stdin=True, command=pod_command,
stdout=True, tty=False, stderr=True,
_preload_content=False, **self.container_arg) stdin=True,
stdout=True,
tty=False,
_preload_content=False,
**self.container_arg
)
errors = [] errors = []
with open(dest_file, 'wb') as fh: with open(dest_file, "wb") as fh:
while self.response._connected: while self.response._connected:
self.read() self.read()
if self.stdout: if self.stdout:
@@ -279,35 +319,57 @@ class K8SCopyFromPod(K8SCopy):
if self.stderr: if self.stderr:
errors.append(self.stderr) errors.append(self.stderr)
if errors: if errors:
self.module.fail_json(msg="Failed to copy file from Pod: {0}".format(''.join(errors))) self.module.fail_json(
self.module.exit_json(changed=True, result="{0} successfully copied locally into {1}".format(self.remote_path, self.local_path)) msg="Failed to copy file from Pod: {0}".format("".join(errors))
)
self.module.exit_json(
changed=True,
result="{0} successfully copied locally into {1}".format(
self.remote_path, self.local_path
),
)
def run(self): def run(self):
try: try:
self.list_remote_files() self.list_remote_files()
if self.files_to_copy == []: if self.files_to_copy == []:
self.module.exit_json(changed=False, warning="No file found from directory '{0}' into remote Pod.".format(self.remote_path)) self.module.exit_json(
changed=False,
warning="No file found from directory '{0}' into remote Pod.".format(
self.remote_path
),
)
self.copy() self.copy()
except Exception as e: except Exception as e:
self.module.fail_json(msg="Failed to copy file/directory from Pod due to: {0}".format(to_native(e))) self.module.fail_json(
msg="Failed to copy file/directory from Pod due to: {0}".format(
to_native(e)
)
)
class K8SCopyToPod(K8SCopy): class K8SCopyToPod(K8SCopy):
""" """
Copy files/directory from local filesystem into remote Pod Copy files/directory from local filesystem into remote Pod
""" """
def __init__(self, module, client): def __init__(self, module, client):
super(K8SCopyToPod, self).__init__(module, client) super(K8SCopyToPod, self).__init__(module, client)
self.files_to_copy = list() self.files_to_copy = list()
def run_from_pod(self, command): def run_from_pod(self, command):
response = stream(self.api_instance.connect_get_namespaced_pod_exec, response = stream(
self.name, self.api_instance.connect_get_namespaced_pod_exec,
self.namespace, self.name,
command=command, self.namespace,
stderr=True, stdin=False, command=command,
stdout=True, tty=False, stderr=True,
_preload_content=False, **self.container_arg) stdin=False,
stdout=True,
tty=False,
_preload_content=False,
**self.container_arg
)
errors = [] errors = []
while response.is_open(): while response.is_open():
response.update(timeout=1) response.update(timeout=1)
@@ -317,24 +379,31 @@ class K8SCopyToPod(K8SCopy):
err = response.read_channel(ERROR_CHANNEL) err = response.read_channel(ERROR_CHANNEL)
err = yaml.safe_load(err) err = yaml.safe_load(err)
response.close() response.close()
if err['status'] != 'Success': if err["status"] != "Success":
self.module.fail_json(msg="Failed to run {0} on Pod.".format(command), errors=errors) self.module.fail_json(
msg="Failed to run {0} on Pod.".format(command), errors=errors
)
def is_remote_path_dir(self): def is_remote_path_dir(self):
pod_command = ['test', '-d', self.remote_path] pod_command = ["test", "-d", self.remote_path]
response = stream(self.api_instance.connect_get_namespaced_pod_exec, response = stream(
self.name, self.api_instance.connect_get_namespaced_pod_exec,
self.namespace, self.name,
command=pod_command, self.namespace,
stdout=True, stderr=True, command=pod_command,
stdin=False, tty=False, stdout=True,
_preload_content=False, **self.container_arg) stderr=True,
stdin=False,
tty=False,
_preload_content=False,
**self.container_arg
)
while response.is_open(): while response.is_open():
response.update(timeout=1) response.update(timeout=1)
err = response.read_channel(ERROR_CHANNEL) err = response.read_channel(ERROR_CHANNEL)
err = yaml.safe_load(err) err = yaml.safe_load(err)
response.close() response.close()
if err['status'] == 'Success': if err["status"] == "Success":
return True return True
return False return False
@@ -355,33 +424,52 @@ class K8SCopyToPod(K8SCopy):
src_file = self.named_temp_file.name src_file = self.named_temp_file.name
else: else:
if not os.path.exists(self.local_path): if not os.path.exists(self.local_path):
self.module.fail_json(msg="{0} does not exist in local filesystem".format(self.local_path)) self.module.fail_json(
msg="{0} does not exist in local filesystem".format(
self.local_path
)
)
if not os.access(self.local_path, os.R_OK): if not os.access(self.local_path, os.R_OK):
self.module.fail_json(msg="{0} not readable".format(self.local_path)) self.module.fail_json(
msg="{0} not readable".format(self.local_path)
)
if self.is_remote_path_dir(): if self.is_remote_path_dir():
if self.content: if self.content:
self.module.fail_json(msg="When content is specified, remote path should not be an existing directory") self.module.fail_json(
msg="When content is specified, remote path should not be an existing directory"
)
else: else:
dest_file = os.path.join(dest_file, os.path.basename(src_file)) dest_file = os.path.join(dest_file, os.path.basename(src_file))
if self.no_preserve: if self.no_preserve:
tar_command = ['tar', '--no-same-permissions', '--no-same-owner', '-xmf', '-'] tar_command = [
"tar",
"--no-same-permissions",
"--no-same-owner",
"-xmf",
"-",
]
else: else:
tar_command = ['tar', '-xmf', '-'] tar_command = ["tar", "-xmf", "-"]
if dest_file.startswith("/"): if dest_file.startswith("/"):
tar_command.extend(['-C', '/']) tar_command.extend(["-C", "/"])
response = stream(self.api_instance.connect_get_namespaced_pod_exec, response = stream(
self.name, self.api_instance.connect_get_namespaced_pod_exec,
self.namespace, self.name,
command=tar_command, self.namespace,
stderr=True, stdin=True, command=tar_command,
stdout=True, tty=False, stderr=True,
_preload_content=False, **self.container_arg) stdin=True,
stdout=True,
tty=False,
_preload_content=False,
**self.container_arg
)
with TemporaryFile() as tar_buffer: with TemporaryFile() as tar_buffer:
with tarfile.open(fileobj=tar_buffer, mode='w') as tar: with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
tar.add(src_file, dest_file) tar.add(src_file, dest_file)
tar_buffer.seek(0) tar_buffer.seek(0)
commands = [] commands = []
@@ -407,36 +495,59 @@ class K8SCopyToPod(K8SCopy):
response.close() response.close()
if stderr: if stderr:
self.close_temp_file() self.close_temp_file()
self.module.fail_json(command=tar_command, msg="Failed to copy local file/directory into Pod due to: {0}".format(''.join(stderr))) self.module.fail_json(
command=tar_command,
msg="Failed to copy local file/directory into Pod due to: {0}".format(
"".join(stderr)
),
)
self.close_temp_file() self.close_temp_file()
if self.content: if self.content:
self.module.exit_json(changed=True, result="Content successfully copied into {0} on remote Pod".format(self.remote_path)) self.module.exit_json(
self.module.exit_json(changed=True, result="{0} successfully copied into remote Pod into {1}".format(self.local_path, self.remote_path)) changed=True,
result="Content successfully copied into {0} on remote Pod".format(
self.remote_path
),
)
self.module.exit_json(
changed=True,
result="{0} successfully copied into remote Pod into {1}".format(
self.local_path, self.remote_path
),
)
except Exception as e: except Exception as e:
self.module.fail_json(msg="Failed to copy local file/directory into Pod due to: {0}".format(to_native(e))) self.module.fail_json(
msg="Failed to copy local file/directory into Pod due to: {0}".format(
to_native(e)
)
)
def check_pod(k8s_ansible_mixin, module): def check_pod(k8s_ansible_mixin, module):
resource = k8s_ansible_mixin.find_resource("Pod", None, True) resource = k8s_ansible_mixin.find_resource("Pod", None, True)
namespace = module.params.get('namespace') namespace = module.params.get("namespace")
name = module.params.get('pod') name = module.params.get("pod")
container = module.params.get('container') container = module.params.get("container")
def _fail(exc): def _fail(exc):
arg = {} arg = {}
if hasattr(exc, 'body'): if hasattr(exc, "body"):
msg = "Namespace={0} Kind=Pod Name={1}: Failed requested object: {2}".format(namespace, name, exc.body) msg = "Namespace={0} Kind=Pod Name={1}: Failed requested object: {2}".format(
namespace, name, exc.body
)
else: else:
msg = to_native(exc) msg = to_native(exc)
for attr in ['status', 'reason']: for attr in ["status", "reason"]:
if hasattr(exc, attr): if hasattr(exc, attr):
arg[attr] = getattr(exc, attr) arg[attr] = getattr(exc, attr)
module.fail_json(msg=msg, **arg) module.fail_json(msg=msg, **arg)
try: try:
result = resource.get(name=name, namespace=namespace) result = resource.get(name=name, namespace=namespace)
containers = [c['name'] for c in result.to_dict()['status']['containerStatuses']] containers = [
c["name"] for c in result.to_dict()["status"]["containerStatuses"]
]
if container and container not in containers: if container and container not in containers:
module.fail_json(msg="Pod has no container {0}".format(container)) module.fail_json(msg="Pod has no container {0}".format(container))
return containers return containers
@@ -457,12 +568,14 @@ def execute_module(module):
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
containers = check_pod(k8s_ansible_mixin, module) containers = check_pod(k8s_ansible_mixin, module)
if len(containers) > 1 and module.params.get('container') is None: if len(containers) > 1 and module.params.get("container") is None:
module.fail_json(msg="Pod contains more than 1 container, option 'container' should be set") module.fail_json(
msg="Pod contains more than 1 container, option 'container' should be set"
)
try: try:
load_class = {'to_pod': K8SCopyToPod, 'from_pod': K8SCopyFromPod} load_class = {"to_pod": K8SCopyToPod, "from_pod": K8SCopyFromPod}
state = module.params.get('state') state = module.params.get("state")
k8s_copy = load_class.get(state)(module, k8s_ansible_mixin.client) k8s_copy = load_class.get(state)(module, k8s_ansible_mixin.client)
k8s_copy.run() k8s_copy.run()
except Exception as e: except Exception as e:
@@ -471,23 +584,29 @@ def execute_module(module):
def main(): def main():
argument_spec = copy.deepcopy(AUTH_ARG_SPEC) argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec['namespace'] = {'type': 'str', 'required': True} argument_spec["namespace"] = {"type": "str", "required": True}
argument_spec['pod'] = {'type': 'str', 'required': True} argument_spec["pod"] = {"type": "str", "required": True}
argument_spec['container'] = {} argument_spec["container"] = {}
argument_spec['remote_path'] = {'type': 'path', 'required': True} argument_spec["remote_path"] = {"type": "path", "required": True}
argument_spec['local_path'] = {'type': 'path'} argument_spec["local_path"] = {"type": "path"}
argument_spec['content'] = {'type': 'str'} argument_spec["content"] = {"type": "str"}
argument_spec['state'] = {'type': 'str', 'default': 'to_pod', 'choices': ['to_pod', 'from_pod']} argument_spec["state"] = {
argument_spec['no_preserve'] = {'type': 'bool', 'default': False} "type": "str",
"default": "to_pod",
"choices": ["to_pod", "from_pod"],
}
argument_spec["no_preserve"] = {"type": "bool", "default": False}
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(
mutually_exclusive=[('local_path', 'content')], argument_spec=argument_spec,
required_if=[('state', 'from_pod', ['local_path'])], mutually_exclusive=[("local_path", "content")],
required_one_of=[['local_path', 'content']], required_if=[("state", "from_pod", ["local_path"])],
supports_check_mode=True) required_one_of=[["local_path", "content"]],
supports_check_mode=True,
)
execute_module(module) execute_module(module)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_drain module: k8s_drain
@@ -83,9 +83,9 @@ options:
requirements: requirements:
- python >= 3.6 - python >= 3.6
- kubernetes >= 12.0.0 - kubernetes >= 12.0.0
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Drain node "foo", even if there are pods not managed by a ReplicationController, Job, or DaemonSet on it. - name: Drain node "foo", even if there are pods not managed by a ReplicationController, Job, or DaemonSet on it.
kubernetes.core.k8s_drain: kubernetes.core.k8s_drain:
state: drain state: drain
@@ -109,20 +109,24 @@ EXAMPLES = r'''
state: cordon state: cordon
name: foo name: foo
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- The node status and the number of pods deleted. - The node status and the number of pods deleted.
returned: success returned: success
type: str type: str
''' """
import copy import copy
from datetime import datetime from datetime import datetime
import time import time
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
)
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
try: try:
@@ -144,7 +148,7 @@ def filter_pods(pods, force, ignore_daemonset):
continue continue
# Any finished pod can be deleted # Any finished pod can be deleted
if pod.status.phase in ('Succeeded', 'Failed'): if pod.status.phase in ("Succeeded", "Failed"):
to_delete.append((pod.metadata.namespace, pod.metadata.name)) to_delete.append((pod.metadata.namespace, pod.metadata.name))
continue continue
@@ -167,19 +171,29 @@ def filter_pods(pods, force, ignore_daemonset):
warnings, errors = [], [] warnings, errors = [], []
if unmanaged: if unmanaged:
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in unmanaged]) pod_names = ",".join([pod[0] + "/" + pod[1] for pod in unmanaged])
if not force: if not force:
errors.append("cannot delete Pods not managed by ReplicationController, ReplicaSet, Job," errors.append(
" DaemonSet or StatefulSet (use option force set to yes): {0}.".format(pod_names)) "cannot delete Pods not managed by ReplicationController, ReplicaSet, Job,"
" DaemonSet or StatefulSet (use option force set to yes): {0}.".format(
pod_names
)
)
else: else:
# Pod not managed will be deleted as 'force' is true # Pod not managed will be deleted as 'force' is true
warnings.append("Deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: {0}.".format(pod_names)) warnings.append(
"Deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: {0}.".format(
pod_names
)
)
to_delete += unmanaged to_delete += unmanaged
# mirror pods warning # mirror pods warning
if mirror: if mirror:
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in mirror]) pod_names = ",".join([pod[0] + "/" + pod[1] for pod in mirror])
warnings.append("cannot delete mirror Pods using API server: {0}.".format(pod_names)) warnings.append(
"cannot delete mirror Pods using API server: {0}.".format(pod_names)
)
# local storage # local storage
if localStorage: if localStorage:
@@ -187,19 +201,24 @@ def filter_pods(pods, force, ignore_daemonset):
# DaemonSet managed Pods # DaemonSet managed Pods
if daemonSet: if daemonSet:
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in daemonSet]) pod_names = ",".join([pod[0] + "/" + pod[1] for pod in daemonSet])
if not ignore_daemonset: if not ignore_daemonset:
errors.append("cannot delete DaemonSet-managed Pods (use option ignore_daemonset set to yes): {0}.".format(pod_names)) errors.append(
"cannot delete DaemonSet-managed Pods (use option ignore_daemonset set to yes): {0}.".format(
pod_names
)
)
else: else:
warnings.append("Ignoring DaemonSet-managed Pods: {0}.".format(pod_names)) warnings.append("Ignoring DaemonSet-managed Pods: {0}.".format(pod_names))
return to_delete, warnings, errors return to_delete, warnings, errors
class K8sDrainAnsible(object): class K8sDrainAnsible(object):
def __init__(self, module): def __init__(self, module):
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
self._module = module self._module = module
self._k8s_ansible_mixin = K8sAnsibleMixin(module) self._k8s_ansible_mixin = K8sAnsibleMixin(module)
@@ -215,17 +234,25 @@ class K8sDrainAnsible(object):
self._k8s_ansible_mixin.warn = self._module.warn self._k8s_ansible_mixin.warn = self._module.warn
self._k8s_ansible_mixin.warnings = [] self._k8s_ansible_mixin.warnings = []
self._api_instance = core_v1_api.CoreV1Api(self._k8s_ansible_mixin.client.client) self._api_instance = core_v1_api.CoreV1Api(
self._k8s_ansible_mixin.client.client
)
self._k8s_ansible_mixin.check_library_version() self._k8s_ansible_mixin.check_library_version()
# delete options # delete options
self._drain_options = module.params.get('delete_options', {}) self._drain_options = module.params.get("delete_options", {})
self._delete_options = None self._delete_options = None
if self._drain_options.get('terminate_grace_period'): if self._drain_options.get("terminate_grace_period"):
self._delete_options = {} self._delete_options = {}
self._delete_options.update({'apiVersion': 'v1'}) self._delete_options.update({"apiVersion": "v1"})
self._delete_options.update({'kind': 'DeleteOptions'}) self._delete_options.update({"kind": "DeleteOptions"})
self._delete_options.update({'gracePeriodSeconds': self._drain_options.get('terminate_grace_period')}) self._delete_options.update(
{
"gracePeriodSeconds": self._drain_options.get(
"terminate_grace_period"
)
}
)
self._changed = False self._changed = False
@@ -241,13 +268,17 @@ class K8sDrainAnsible(object):
if not pod: if not pod:
pod = pods.pop() pod = pods.pop()
try: try:
response = self._api_instance.read_namespaced_pod(namespace=pod[0], name=pod[1]) response = self._api_instance.read_namespaced_pod(
namespace=pod[0], name=pod[1]
)
if not response: if not response:
pod = None pod = None
time.sleep(wait_sleep) time.sleep(wait_sleep)
except ApiException as exc: except ApiException as exc:
if exc.reason != "Not Found": if exc.reason != "Not Found":
self._module.fail_json(msg="Exception raised: {0}".format(exc.reason)) self._module.fail_json(
msg="Exception raised: {0}".format(exc.reason)
)
pod = None pod = None
except Exception as e: except Exception as e:
self._module.fail_json(msg="Exception raised: {0}".format(to_native(e))) self._module.fail_json(msg="Exception raised: {0}".format(to_native(e)))
@@ -257,37 +288,50 @@ class K8sDrainAnsible(object):
def evict_pods(self, pods): def evict_pods(self, pods):
for namespace, name in pods: for namespace, name in pods:
definition = { definition = {"metadata": {"name": name, "namespace": namespace}}
'metadata': {
'name': name,
'namespace': namespace
}
}
if self._delete_options: if self._delete_options:
definition.update({'delete_options': self._delete_options}) definition.update({"delete_options": self._delete_options})
try: try:
if self._drain_options.get('disable_eviction'): if self._drain_options.get("disable_eviction"):
body = V1DeleteOptions(**definition) body = V1DeleteOptions(**definition)
self._api_instance.delete_namespaced_pod(name=name, namespace=namespace, body=body) self._api_instance.delete_namespaced_pod(
name=name, namespace=namespace, body=body
)
else: else:
body = V1beta1Eviction(**definition) body = V1beta1Eviction(**definition)
self._api_instance.create_namespaced_pod_eviction(name=name, namespace=namespace, body=body) self._api_instance.create_namespaced_pod_eviction(
name=name, namespace=namespace, body=body
)
self._changed = True self._changed = True
except ApiException as exc: except ApiException as exc:
if exc.reason != "Not Found": if exc.reason != "Not Found":
self._module.fail_json(msg="Failed to delete pod {0}/{1} due to: {2}".format(namespace, name, exc.reason)) self._module.fail_json(
msg="Failed to delete pod {0}/{1} due to: {2}".format(
namespace, name, exc.reason
)
)
except Exception as exc: except Exception as exc:
self._module.fail_json(msg="Failed to delete pod {0}/{1} due to: {2}".format(namespace, name, to_native(exc))) self._module.fail_json(
msg="Failed to delete pod {0}/{1} due to: {2}".format(
namespace, name, to_native(exc)
)
)
def delete_or_evict_pods(self, node_unschedulable): def delete_or_evict_pods(self, node_unschedulable):
# Mark node as unschedulable # Mark node as unschedulable
result = [] result = []
if not node_unschedulable: if not node_unschedulable:
self.patch_node(unschedulable=True) self.patch_node(unschedulable=True)
result.append("node {0} marked unschedulable.".format(self._module.params.get('name'))) result.append(
"node {0} marked unschedulable.".format(self._module.params.get("name"))
)
self._changed = True self._changed = True
else: else:
result.append("node {0} already marked unschedulable.".format(self._module.params.get('name'))) result.append(
"node {0} already marked unschedulable.".format(
self._module.params.get("name")
)
)
def _revert_node_patch(): def _revert_node_patch():
if self._changed: if self._changed:
@@ -295,77 +339,109 @@ class K8sDrainAnsible(object):
self.patch_node(unschedulable=False) self.patch_node(unschedulable=False)
try: try:
field_selector = "spec.nodeName={name}".format(name=self._module.params.get('name')) field_selector = "spec.nodeName={name}".format(
pod_list = self._api_instance.list_pod_for_all_namespaces(field_selector=field_selector) name=self._module.params.get("name")
)
pod_list = self._api_instance.list_pod_for_all_namespaces(
field_selector=field_selector
)
# Filter pods # Filter pods
force = self._drain_options.get('force', False) force = self._drain_options.get("force", False)
ignore_daemonset = self._drain_options.get('ignore_daemonsets', False) ignore_daemonset = self._drain_options.get("ignore_daemonsets", False)
pods, warnings, errors = filter_pods(pod_list.items, force, ignore_daemonset) pods, warnings, errors = filter_pods(
pod_list.items, force, ignore_daemonset
)
if errors: if errors:
_revert_node_patch() _revert_node_patch()
self._module.fail_json(msg="Pod deletion errors: {0}".format(" ".join(errors))) self._module.fail_json(
msg="Pod deletion errors: {0}".format(" ".join(errors))
)
except ApiException as exc: except ApiException as exc:
if exc.reason != "Not Found": if exc.reason != "Not Found":
_revert_node_patch() _revert_node_patch()
self._module.fail_json(msg="Failed to list pod from node {name} due to: {reason}".format( self._module.fail_json(
name=self._module.params.get('name'), reason=exc.reason), status=exc.status) msg="Failed to list pod from node {name} due to: {reason}".format(
name=self._module.params.get("name"), reason=exc.reason
),
status=exc.status,
)
pods = [] pods = []
except Exception as exc: except Exception as exc:
_revert_node_patch() _revert_node_patch()
self._module.fail_json(msg="Failed to list pod from node {name} due to: {error}".format( self._module.fail_json(
name=self._module.params.get('name'), error=to_native(exc))) msg="Failed to list pod from node {name} due to: {error}".format(
name=self._module.params.get("name"), error=to_native(exc)
)
)
# Delete Pods # Delete Pods
if pods: if pods:
self.evict_pods(pods) self.evict_pods(pods)
number_pod = len(pods) number_pod = len(pods)
if self._drain_options.get('wait_timeout') is not None: if self._drain_options.get("wait_timeout") is not None:
warn = self.wait_for_pod_deletion(pods, warn = self.wait_for_pod_deletion(
self._drain_options.get('wait_timeout'), pods,
self._drain_options.get('wait_sleep')) self._drain_options.get("wait_timeout"),
self._drain_options.get("wait_sleep"),
)
if warn: if warn:
warnings.append(warn) warnings.append(warn)
result.append("{0} Pod(s) deleted from node.".format(number_pod)) result.append("{0} Pod(s) deleted from node.".format(number_pod))
if warnings: if warnings:
return dict(result=' '.join(result), warnings=warnings) return dict(result=" ".join(result), warnings=warnings)
return dict(result=' '.join(result)) return dict(result=" ".join(result))
def patch_node(self, unschedulable): def patch_node(self, unschedulable):
body = { body = {"spec": {"unschedulable": unschedulable}}
'spec': {'unschedulable': unschedulable}
}
try: try:
self._api_instance.patch_node(name=self._module.params.get('name'), body=body) self._api_instance.patch_node(
name=self._module.params.get("name"), body=body
)
except Exception as exc: except Exception as exc:
self._module.fail_json(msg="Failed to patch node due to: {0}".format(to_native(exc))) self._module.fail_json(
msg="Failed to patch node due to: {0}".format(to_native(exc))
)
def execute_module(self): def execute_module(self):
state = self._module.params.get('state') state = self._module.params.get("state")
name = self._module.params.get('name') name = self._module.params.get("name")
try: try:
node = self._api_instance.read_node(name=name) node = self._api_instance.read_node(name=name)
except ApiException as exc: except ApiException as exc:
if exc.reason == "Not Found": if exc.reason == "Not Found":
self._module.fail_json(msg="Node {0} not found.".format(name)) self._module.fail_json(msg="Node {0} not found.".format(name))
self._module.fail_json(msg="Failed to retrieve node '{0}' due to: {1}".format(name, exc.reason), status=exc.status) self._module.fail_json(
msg="Failed to retrieve node '{0}' due to: {1}".format(
name, exc.reason
),
status=exc.status,
)
except Exception as exc: except Exception as exc:
self._module.fail_json(msg="Failed to retrieve node '{0}' due to: {1}".format(name, to_native(exc))) self._module.fail_json(
msg="Failed to retrieve node '{0}' due to: {1}".format(
name, to_native(exc)
)
)
result = {} result = {}
if state == "cordon": if state == "cordon":
if node.spec.unschedulable: if node.spec.unschedulable:
self._module.exit_json(result="node {0} already marked unschedulable.".format(name)) self._module.exit_json(
result="node {0} already marked unschedulable.".format(name)
)
self.patch_node(unschedulable=True) self.patch_node(unschedulable=True)
result['result'] = "node {0} marked unschedulable.".format(name) result["result"] = "node {0} marked unschedulable.".format(name)
self._changed = True self._changed = True
elif state == "uncordon": elif state == "uncordon":
if not node.spec.unschedulable: if not node.spec.unschedulable:
self._module.exit_json(result="node {0} already marked schedulable.".format(name)) self._module.exit_json(
result="node {0} already marked schedulable.".format(name)
)
self.patch_node(unschedulable=False) self.patch_node(unschedulable=False)
result['result'] = "node {0} marked schedulable.".format(name) result["result"] = "node {0} marked schedulable.".format(name)
self._changed = True self._changed = True
else: else:
@@ -384,16 +460,16 @@ def argspec():
state=dict(default="drain", choices=["cordon", "drain", "uncordon"]), state=dict(default="drain", choices=["cordon", "drain", "uncordon"]),
name=dict(required=True), name=dict(required=True),
delete_options=dict( delete_options=dict(
type='dict', type="dict",
default={}, default={},
options=dict( options=dict(
terminate_grace_period=dict(type='int'), terminate_grace_period=dict(type="int"),
force=dict(type='bool', default=False), force=dict(type="bool", default=False),
ignore_daemonsets=dict(type='bool', default=False), ignore_daemonsets=dict(type="bool", default=False),
disable_eviction=dict(type='bool', default=False), disable_eviction=dict(type="bool", default=False),
wait_timeout=dict(type='int'), wait_timeout=dict(type="int"),
wait_sleep=dict(type='int', default=5), wait_sleep=dict(type="int", default=5),
) ),
), ),
) )
) )
@@ -407,5 +483,5 @@ def main():
k8s_drain.execute_module() k8s_drain.execute_module()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_exec module: k8s_exec
@@ -63,9 +63,9 @@ options:
- The command to execute - The command to execute
type: str type: str
required: yes required: yes
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Execute a command - name: Execute a command
kubernetes.core.k8s_exec: kubernetes.core.k8s_exec:
namespace: myproject namespace: myproject
@@ -84,9 +84,9 @@ EXAMPLES = r'''
debug: debug:
msg: "cmd failed" msg: "cmd failed"
when: command_status.rc != 0 when: command_status.rc != 0
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- The command object - The command object
@@ -112,7 +112,7 @@ result:
return_code: return_code:
description: The command status code. This attribute is deprecated and will be removed in a future release. Please use rc instead. description: The command status code. This attribute is deprecated and will be removed in a future release. Please use rc instead.
type: int type: int
''' """
import copy import copy
import shlex import shlex
@@ -123,10 +123,12 @@ except ImportError:
# ImportError are managed by the common module already. # ImportError are managed by the common module already.
pass pass
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
AUTH_ARG_SPEC AUTH_ARG_SPEC,
) )
try: try:
@@ -139,10 +141,10 @@ except ImportError:
def argspec(): def argspec():
spec = copy.deepcopy(AUTH_ARG_SPEC) spec = copy.deepcopy(AUTH_ARG_SPEC)
spec['namespace'] = dict(type='str', required=True) spec["namespace"] = dict(type="str", required=True)
spec['pod'] = dict(type='str', required=True) spec["pod"] = dict(type="str", required=True)
spec['container'] = dict(type='str') spec["container"] = dict(type="str")
spec['command'] = dict(type='str', required=True) spec["command"] = dict(type="str", required=True)
return spec return spec
@@ -153,8 +155,8 @@ def execute_module(module, k8s_ansible_mixin):
# hack because passing the container as None breaks things # hack because passing the container as None breaks things
optional_kwargs = {} optional_kwargs = {}
if module.params.get('container'): if module.params.get("container"):
optional_kwargs['container'] = module.params['container'] optional_kwargs["container"] = module.params["container"]
try: try:
resp = stream( resp = stream(
api.connect_get_namespaced_pod_exec, api.connect_get_namespaced_pod_exec,
@@ -165,10 +167,14 @@ def execute_module(module, k8s_ansible_mixin):
stderr=True, stderr=True,
stdin=False, stdin=False,
tty=False, tty=False,
_preload_content=False, **optional_kwargs) _preload_content=False,
**optional_kwargs
)
except Exception as e: except Exception as e:
module.fail_json(msg="Failed to execute on pod %s" module.fail_json(
" due to : %s" % (module.params.get('pod'), to_native(e))) msg="Failed to execute on pod %s"
" due to : %s" % (module.params.get("pod"), to_native(e))
)
stdout, stderr, rc = [], [], 0 stdout, stderr, rc = [], [], 0
while resp.is_open(): while resp.is_open():
resp.update(timeout=1) resp.update(timeout=1)
@@ -178,34 +184,37 @@ def execute_module(module, k8s_ansible_mixin):
stderr.append(resp.read_stderr()) stderr.append(resp.read_stderr())
err = resp.read_channel(3) err = resp.read_channel(3)
err = yaml.safe_load(err) err = yaml.safe_load(err)
if err['status'] == 'Success': if err["status"] == "Success":
rc = 0 rc = 0
else: else:
rc = int(err['details']['causes'][0]['message']) rc = int(err["details"]["causes"][0]["message"])
module.deprecate("The 'return_code' return key is deprecated. Please use 'rc' instead.", version="4.0.0", collection_name="kubernetes.core") module.deprecate(
"The 'return_code' return key is deprecated. Please use 'rc' instead.",
version="4.0.0",
collection_name="kubernetes.core",
)
module.exit_json( module.exit_json(
# Some command might change environment, but ultimately failing at end # Some command might change environment, but ultimately failing at end
changed=True, changed=True,
stdout="".join(stdout), stdout="".join(stdout),
stderr="".join(stderr), stderr="".join(stderr),
rc=rc, rc=rc,
return_code=rc return_code=rc,
) )
def main(): def main():
module = AnsibleModule( module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True,)
argument_spec=argspec(),
supports_check_mode=True,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_info module: k8s_info
short_description: Describe Kubernetes (K8s) objects short_description: Describe Kubernetes (K8s) objects
@@ -52,9 +52,9 @@ requirements:
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Get an existing Service object - name: Get an existing Service object
kubernetes.core.k8s_info: kubernetes.core.k8s_info:
api_version: v1 api_version: v1
@@ -109,9 +109,9 @@ EXAMPLES = r'''
namespace: default namespace: default
wait_sleep: 10 wait_sleep: 10
wait_timeout: 360 wait_timeout: 360
''' """
RETURN = r''' RETURN = r"""
api_found: api_found:
description: description:
- Whether the specified api_version and kind were successfully mapped to an existing API on the targeted cluster. - Whether the specified api_version and kind were successfully mapped to an existing API on the targeted cluster.
@@ -144,12 +144,17 @@ resources:
description: Current status details for the object. description: Current status details for the object.
returned: success returned: success
type: dict type: dict
''' """
import copy import copy
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC, WAIT_ARG_SPEC) AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
WAIT_ARG_SPEC,
)
def execute_module(module, k8s_ansible_mixin): def execute_module(module, k8s_ansible_mixin):
@@ -174,11 +179,11 @@ def argspec():
args.update( args.update(
dict( dict(
kind=dict(required=True), kind=dict(required=True),
api_version=dict(default='v1', aliases=['api', 'version']), api_version=dict(default="v1", aliases=["api", "version"]),
name=dict(), name=dict(),
namespace=dict(), namespace=dict(),
label_selectors=dict(type='list', elements='str', default=[]), label_selectors=dict(type="list", elements="str", default=[]),
field_selectors=dict(type='list', elements='str', default=[]), field_selectors=dict(type="list", elements="str", default=[]),
) )
) )
return args return args
@@ -187,12 +192,14 @@ def argspec():
def main(): def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_json_patch module: k8s_json_patch
short_description: Apply JSON patch operations to existing objects short_description: Apply JSON patch operations to existing objects
@@ -66,9 +66,9 @@ requirements:
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
- "jsonpatch" - "jsonpatch"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Apply multiple patch operations to an existing Pod - name: Apply multiple patch operations to an existing Pod
kubernetes.core.k8s_json_patch: kubernetes.core.k8s_json_patch:
kind: Pod kind: Pod
@@ -81,9 +81,9 @@ EXAMPLES = r'''
- op: replace - op: replace
patch: /spec/containers/0/image patch: /spec/containers/0/image
value: nginx value: nginx
''' """
RETURN = r''' RETURN = r"""
result: result:
description: The modified object. description: The modified object.
returned: success returned: success
@@ -122,17 +122,24 @@ error:
"msg": "Failed to import the required Python library (jsonpatch) ...", "msg": "Failed to import the required Python library (jsonpatch) ...",
"exception": "Traceback (most recent call last): ..." "exception": "Traceback (most recent call last): ..."
} }
''' """
import copy import copy
import traceback import traceback
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, WAIT_ARG_SPEC AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
WAIT_ARG_SPEC,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
get_api_client, K8sAnsibleMixin) get_api_client,
K8sAnsibleMixin,
)
try: try:
from kubernetes.dynamic.exceptions import DynamicApiError from kubernetes.dynamic.exceptions import DynamicApiError
@@ -143,6 +150,7 @@ except ImportError:
JSON_PATCH_IMPORT_ERR = None JSON_PATCH_IMPORT_ERR = None
try: try:
import jsonpatch import jsonpatch
HAS_JSON_PATCH = True HAS_JSON_PATCH = True
except ImportError: except ImportError:
HAS_JSON_PATCH = False HAS_JSON_PATCH = False
@@ -150,33 +158,18 @@ except ImportError:
JSON_PATCH_ARGS = { JSON_PATCH_ARGS = {
'api_version': { "api_version": {"default": "v1", "aliases": ["api", "version"]},
'default': 'v1', "kind": {"type": "str", "required": True},
'aliases': ['api', 'version'], "namespace": {"type": "str"},
}, "name": {"type": "str", "required": True},
"kind": { "patch": {"type": "list", "required": True, "elements": "dict"},
"type": "str",
"required": True,
},
"namespace": {
"type": "str",
},
"name": {
"type": "str",
"required": True,
},
"patch": {
"type": "list",
"required": True,
"elements": "dict",
},
} }
def json_patch(existing, patch): def json_patch(existing, patch):
if not HAS_JSON_PATCH: if not HAS_JSON_PATCH:
error = { error = {
"msg": missing_required_lib('jsonpatch'), "msg": missing_required_lib("jsonpatch"),
"exception": JSON_PATCH_IMPORT_ERR, "exception": JSON_PATCH_IMPORT_ERR,
} }
return None, error return None, error
@@ -185,16 +178,10 @@ def json_patch(existing, patch):
patched = patch.apply(existing) patched = patch.apply(existing)
return patched, None return patched, None
except jsonpatch.InvalidJsonPatch as e: except jsonpatch.InvalidJsonPatch as e:
error = { error = {"msg": "Invalid JSON patch", "exception": e}
"msg": "Invalid JSON patch",
"exception": e
}
return None, error return None, error
except jsonpatch.JsonPatchConflict as e: except jsonpatch.JsonPatchConflict as e:
error = { error = {"msg": "Patch could not be applied due to a conflict", "exception": e}
"msg": "Patch could not be applied due to a conflict",
"exception": e
}
return None, error return None, error
@@ -209,15 +196,14 @@ def execute_module(k8s_module, module):
wait_sleep = module.params.get("wait_sleep") wait_sleep = module.params.get("wait_sleep")
wait_timeout = module.params.get("wait_timeout") wait_timeout = module.params.get("wait_timeout")
wait_condition = None wait_condition = None
if module.params.get("wait_condition") and module.params.get("wait_condition").get("type"): if module.params.get("wait_condition") and module.params.get("wait_condition").get(
wait_condition = module.params['wait_condition'] "type"
):
wait_condition = module.params["wait_condition"]
# definition is needed for wait # definition is needed for wait
definition = { definition = {
"kind": kind, "kind": kind,
"metadata": { "metadata": {"name": name, "namespace": namespace},
"name": name,
"namespace": namespace,
}
} }
def build_error_msg(kind, name, msg): def build_error_msg(kind, name, msg):
@@ -228,11 +214,18 @@ def execute_module(k8s_module, module):
try: try:
existing = resource.get(name=name, namespace=namespace) existing = resource.get(name=name, namespace=namespace)
except DynamicApiError as exc: except DynamicApiError as exc:
msg = 'Failed to retrieve requested object: {0}'.format(exc.body) msg = "Failed to retrieve requested object: {0}".format(exc.body)
module.fail_json(msg=build_error_msg(kind, name, msg), error=exc.status, status=exc.status, reason=exc.reason) module.fail_json(
msg=build_error_msg(kind, name, msg),
error=exc.status,
status=exc.status,
reason=exc.reason,
)
except ValueError as exc: except ValueError as exc:
msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc)) msg = "Failed to retrieve requested object: {0}".format(to_native(exc))
module.fail_json(msg=build_error_msg(kind, name, msg), error='', status='', reason='') module.fail_json(
msg=build_error_msg(kind, name, msg), error="", status="", reason=""
)
if module.check_mode and not k8s_module.supports_dry_run: if module.check_mode and not k8s_module.supports_dry_run:
obj, error = json_patch(existing.to_dict(), patch) obj, error = json_patch(existing.to_dict(), patch)
@@ -243,18 +236,28 @@ def execute_module(k8s_module, module):
if module.check_mode: if module.check_mode:
params["dry_run"] = "All" params["dry_run"] = "All"
try: try:
obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json", **params).to_dict() obj = resource.patch(
patch,
name=name,
namespace=namespace,
content_type="application/json-patch+json",
**params
).to_dict()
except DynamicApiError as exc: except DynamicApiError as exc:
msg = "Failed to patch existing object: {0}".format(exc.body) msg = "Failed to patch existing object: {0}".format(exc.body)
module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) module.fail_json(
msg=msg, error=exc.status, status=exc.status, reason=exc.reason
)
except Exception as exc: except Exception as exc:
msg = "Failed to patch existing object: {0}".format(exc) msg = "Failed to patch existing object: {0}".format(exc)
module.fail_json(msg=msg, error=to_native(exc), status='', reason='') module.fail_json(msg=msg, error=to_native(exc), status="", reason="")
success = True success = True
result = {"result": obj} result = {"result": obj}
if wait and not module.check_mode: if wait and not module.check_mode:
success, result['result'], result['duration'] = k8s_module.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) success, result["result"], result["duration"] = k8s_module.wait(
resource, definition, wait_sleep, wait_timeout, condition=wait_condition
)
match, diffs = k8s_module.diff_objects(existing.to_dict(), obj) match, diffs = k8s_module.diff_objects(existing.to_dict(), obj)
result["changed"] = not match result["changed"] = not match
if module._diff: if module._diff:

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_log module: k8s_log
short_description: Fetch logs from Kubernetes resources short_description: Fetch logs from Kubernetes resources
@@ -65,9 +65,9 @@ requirements:
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Get a log from a Pod - name: Get a log from a Pod
kubernetes.core.k8s_log: kubernetes.core.k8s_log:
name: example-1 name: example-1
@@ -100,9 +100,9 @@ EXAMPLES = r'''
namespace: testing namespace: testing
name: example name: example
register: log register: log
''' """
RETURN = r''' RETURN = r"""
log: log:
type: str type: str
description: description:
@@ -113,15 +113,20 @@ log_lines:
description: description:
- The log of the object, split on newlines - The log of the object, split on newlines
returned: success returned: success
''' """
import copy import copy
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible.module_utils.six import PY2 from ansible.module_utils.six import PY2
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC, NAME_ARG_SPEC) from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
NAME_ARG_SPEC,
)
def argspec(): def argspec():
@@ -129,55 +134,62 @@ def argspec():
args.update(NAME_ARG_SPEC) args.update(NAME_ARG_SPEC)
args.update( args.update(
dict( dict(
kind=dict(type='str', default='Pod'), kind=dict(type="str", default="Pod"),
container=dict(), container=dict(),
since_seconds=dict(), since_seconds=dict(),
label_selectors=dict(type='list', elements='str', default=[]), label_selectors=dict(type="list", elements="str", default=[]),
) )
) )
return args return args
def execute_module(module, k8s_ansible_mixin): def execute_module(module, k8s_ansible_mixin):
name = module.params.get('name') name = module.params.get("name")
namespace = module.params.get('namespace') namespace = module.params.get("namespace")
label_selector = ','.join(module.params.get('label_selectors', {})) label_selector = ",".join(module.params.get("label_selectors", {}))
if name and label_selector: if name and label_selector:
module.fail(msg='Only one of name or label_selectors can be provided') module.fail(msg="Only one of name or label_selectors can be provided")
resource = k8s_ansible_mixin.find_resource(module.params['kind'], module.params['api_version'], fail=True) resource = k8s_ansible_mixin.find_resource(
v1_pods = k8s_ansible_mixin.find_resource('Pod', 'v1', fail=True) module.params["kind"], module.params["api_version"], fail=True
)
v1_pods = k8s_ansible_mixin.find_resource("Pod", "v1", fail=True)
if 'log' not in resource.subresources: if "log" not in resource.subresources:
if not name: if not name:
module.fail(msg='name must be provided for resources that do not support the log subresource') module.fail(
msg="name must be provided for resources that do not support the log subresource"
)
instance = resource.get(name=name, namespace=namespace) instance = resource.get(name=name, namespace=namespace)
label_selector = ','.join(extract_selectors(module, instance)) label_selector = ",".join(extract_selectors(module, instance))
resource = v1_pods resource = v1_pods
if label_selector: if label_selector:
instances = v1_pods.get(namespace=namespace, label_selector=label_selector) instances = v1_pods.get(namespace=namespace, label_selector=label_selector)
if not instances.items: if not instances.items:
module.fail(msg='No pods in namespace {0} matched selector {1}'.format(namespace, label_selector)) module.fail(
msg="No pods in namespace {0} matched selector {1}".format(
namespace, label_selector
)
)
# This matches the behavior of kubectl when logging pods via a selector # This matches the behavior of kubectl when logging pods via a selector
name = instances.items[0].metadata.name name = instances.items[0].metadata.name
resource = v1_pods resource = v1_pods
kwargs = {} kwargs = {}
if module.params.get('container'): if module.params.get("container"):
kwargs['query_params'] = dict(container=module.params['container']) kwargs["query_params"] = dict(container=module.params["container"])
if module.params.get('since_seconds'): if module.params.get("since_seconds"):
kwargs.setdefault('query_params', {}).update({'sinceSeconds': module.params['since_seconds']}) kwargs.setdefault("query_params", {}).update(
{"sinceSeconds": module.params["since_seconds"]}
)
log = serialize_log(resource.log.get( log = serialize_log(
name=name, resource.log.get(name=name, namespace=namespace, serialize=False, **kwargs)
namespace=namespace, )
serialize=False,
**kwargs
))
module.exit_json(changed=False, log=log, log_lines=log.split('\n')) module.exit_json(changed=False, log=log, log_lines=log.split("\n"))
def extract_selectors(module, instance): def extract_selectors(module, instance):
@@ -185,35 +197,46 @@ def extract_selectors(module, instance):
# https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
selectors = [] selectors = []
if not instance.spec.selector: if not instance.spec.selector:
module.fail(msg='{0} {1} does not support the log subresource directly, and no Pod selector was found on the object'.format( module.fail(
'/'.join(instance.group, instance.apiVersion), instance.kind)) msg="{0} {1} does not support the log subresource directly, and no Pod selector was found on the object".format(
"/".join(instance.group, instance.apiVersion), instance.kind
)
)
if not (instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions): if not (
instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions
):
# A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions # A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions
for k, v in dict(instance.spec.selector).items(): for k, v in dict(instance.spec.selector).items():
selectors.append('{0}={1}'.format(k, v)) selectors.append("{0}={1}".format(k, v))
return selectors return selectors
if instance.spec.selector.matchLabels: if instance.spec.selector.matchLabels:
for k, v in dict(instance.spec.selector.matchLabels).items(): for k, v in dict(instance.spec.selector.matchLabels).items():
selectors.append('{0}={1}'.format(k, v)) selectors.append("{0}={1}".format(k, v))
if instance.spec.selector.matchExpressions: if instance.spec.selector.matchExpressions:
for expression in instance.spec.selector.matchExpressions: for expression in instance.spec.selector.matchExpressions:
operator = expression.operator operator = expression.operator
if operator == 'Exists': if operator == "Exists":
selectors.append(expression.key) selectors.append(expression.key)
elif operator == 'DoesNotExist': elif operator == "DoesNotExist":
selectors.append('!{0}'.format(expression.key)) selectors.append("!{0}".format(expression.key))
elif operator in ['In', 'NotIn']: elif operator in ["In", "NotIn"]:
selectors.append('{key} {operator} {values}'.format( selectors.append(
key=expression.key, "{key} {operator} {values}".format(
operator=operator.lower(), key=expression.key,
values='({0})'.format(', '.join(expression.values)) operator=operator.lower(),
)) values="({0})".format(", ".join(expression.values)),
)
)
else: else:
module.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower())) module.fail(
msg="The k8s_log module does not support the {0} matchExpression operator".format(
operator.lower()
)
)
return selectors return selectors
@@ -221,18 +244,20 @@ def extract_selectors(module, instance):
def serialize_log(response): def serialize_log(response):
if PY2: if PY2:
return response.data return response.data
return response.data.decode('utf8') return response.data.decode("utf8")
def main(): def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -5,10 +5,11 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_rollback module: k8s_rollback
short_description: Rollback Kubernetes (K8S) Deployments and DaemonSets short_description: Rollback Kubernetes (K8S) Deployments and DaemonSets
version_added: "1.0.0" version_added: "1.0.0"
@@ -34,18 +35,18 @@ requirements:
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Rollback a failed deployment - name: Rollback a failed deployment
kubernetes.core.k8s_rollback: kubernetes.core.k8s_rollback:
api_version: apps/v1 api_version: apps/v1
kind: Deployment kind: Deployment
name: web name: web
namespace: testing namespace: testing
''' """
RETURN = r''' RETURN = r"""
rollback_info: rollback_info:
description: description:
- The object that was rolled back. - The object that was rolled back.
@@ -74,25 +75,29 @@ rollback_info:
description: Current status details for the object. description: Current status details for the object.
returned: success returned: success
type: dict type: dict
''' """
import copy import copy
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, NAME_ARG_SPEC) AUTH_ARG_SPEC,
NAME_ARG_SPEC,
)
def get_managed_resource(module): def get_managed_resource(module):
managed_resource = {} managed_resource = {}
kind = module.params['kind'] kind = module.params["kind"]
if kind == "DaemonSet": if kind == "DaemonSet":
managed_resource['kind'] = "ControllerRevision" managed_resource["kind"] = "ControllerRevision"
managed_resource['api_version'] = "apps/v1" managed_resource["api_version"] = "apps/v1"
elif kind == "Deployment": elif kind == "Deployment":
managed_resource['kind'] = "ReplicaSet" managed_resource["kind"] = "ReplicaSet"
managed_resource['api_version'] = "apps/v1" managed_resource["api_version"] = "apps/v1"
else: else:
module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind)) module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind))
return managed_resource return managed_resource
@@ -102,80 +107,89 @@ def execute_module(module, k8s_ansible_mixin):
results = [] results = []
resources = k8s_ansible_mixin.kubernetes_facts( resources = k8s_ansible_mixin.kubernetes_facts(
module.params['kind'], module.params["kind"],
module.params['api_version'], module.params["api_version"],
module.params['name'], module.params["name"],
module.params['namespace'], module.params["namespace"],
module.params['label_selectors'], module.params["label_selectors"],
module.params['field_selectors']) module.params["field_selectors"],
)
for resource in resources['resources']: for resource in resources["resources"]:
result = perform_action(module, k8s_ansible_mixin, resource) result = perform_action(module, k8s_ansible_mixin, resource)
results.append(result) results.append(result)
module.exit_json(**{ module.exit_json(**{"changed": True, "rollback_info": results})
'changed': True,
'rollback_info': results
})
def perform_action(module, k8s_ansible_mixin, resource): def perform_action(module, k8s_ansible_mixin, resource):
if module.params['kind'] == "DaemonSet": if module.params["kind"] == "DaemonSet":
current_revision = resource['metadata']['generation'] current_revision = resource["metadata"]["generation"]
elif module.params['kind'] == "Deployment": elif module.params["kind"] == "Deployment":
current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision'] current_revision = resource["metadata"]["annotations"][
"deployment.kubernetes.io/revision"
]
managed_resource = get_managed_resource(module) managed_resource = get_managed_resource(module)
managed_resources = k8s_ansible_mixin.kubernetes_facts( managed_resources = k8s_ansible_mixin.kubernetes_facts(
managed_resource['kind'], managed_resource["kind"],
managed_resource['api_version'], managed_resource["api_version"],
'', "",
module.params['namespace'], module.params["namespace"],
resource['spec'] resource["spec"]["selector"]["matchLabels"],
['selector'] "",
['matchLabels'], )
'')
prev_managed_resource = get_previous_revision(managed_resources['resources'], prev_managed_resource = get_previous_revision(
current_revision) managed_resources["resources"], current_revision
)
if module.params['kind'] == "Deployment": if module.params["kind"] == "Deployment":
del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash'] del prev_managed_resource["spec"]["template"]["metadata"]["labels"][
"pod-template-hash"
]
resource_patch = [{ resource_patch = [
"op": "replace", {
"path": "/spec/template", "op": "replace",
"value": prev_managed_resource['spec']['template'] "path": "/spec/template",
}, { "value": prev_managed_resource["spec"]["template"],
"op": "replace", },
"path": "/metadata/annotations", {
"value": { "op": "replace",
"deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision'] "path": "/metadata/annotations",
} "value": {
}] "deployment.kubernetes.io/revision": prev_managed_resource[
"metadata"
]["annotations"]["deployment.kubernetes.io/revision"]
},
},
]
api_target = 'deployments' api_target = "deployments"
content_type = 'application/json-patch+json' content_type = "application/json-patch+json"
elif module.params['kind'] == "DaemonSet": elif module.params["kind"] == "DaemonSet":
resource_patch = prev_managed_resource["data"] resource_patch = prev_managed_resource["data"]
api_target = 'daemonsets' api_target = "daemonsets"
content_type = 'application/strategic-merge-patch+json' content_type = "application/strategic-merge-patch+json"
rollback = k8s_ansible_mixin.client.request( rollback = k8s_ansible_mixin.client.request(
"PATCH", "PATCH",
"/apis/{0}/namespaces/{1}/{2}/{3}" "/apis/{0}/namespaces/{1}/{2}/{3}".format(
.format(module.params['api_version'], module.params["api_version"],
module.params['namespace'], module.params["namespace"],
api_target, api_target,
module.params['name']), module.params["name"],
),
body=resource_patch, body=resource_patch,
content_type=content_type) content_type=content_type,
)
result = {'changed': True} result = {"changed": True}
result['method'] = 'patch' result["method"] = "patch"
result['body'] = resource_patch result["body"] = resource_patch
result['resources'] = rollback.to_dict() result["resources"] = rollback.to_dict()
return result return result
@@ -184,8 +198,8 @@ def argspec():
args.update(NAME_ARG_SPEC) args.update(NAME_ARG_SPEC)
args.update( args.update(
dict( dict(
label_selectors=dict(type='list', elements='str', default=[]), label_selectors=dict(type="list", elements="str", default=[]),
field_selectors=dict(type='list', elements='str', default=[]), field_selectors=dict(type="list", elements="str", default=[]),
) )
) )
return args return args
@@ -193,27 +207,40 @@ def argspec():
def get_previous_revision(all_resources, current_revision): def get_previous_revision(all_resources, current_revision):
for resource in all_resources: for resource in all_resources:
if resource['kind'] == 'ReplicaSet': if resource["kind"] == "ReplicaSet":
if int(resource['metadata'] if (
['annotations'] int(
['deployment.kubernetes.io/revision']) == int(current_revision) - 1: resource["metadata"]["annotations"][
"deployment.kubernetes.io/revision"
]
)
== int(current_revision) - 1
):
return resource return resource
elif resource['kind'] == 'ControllerRevision': elif resource["kind"] == "ControllerRevision":
if int(resource['metadata'] if (
['annotations'] int(
['deprecated.daemonset.template.generation']) == int(current_revision) - 1: resource["metadata"]["annotations"][
"deprecated.daemonset.template.generation"
]
)
== int(current_revision) - 1
):
return resource return resource
return None return None
def main(): def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import (K8sAnsibleMixin, get_api_client) from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_scale module: k8s_scale
@@ -48,9 +48,9 @@ requirements:
- "python >= 3.6" - "python >= 3.6"
- "kubernetes >= 12.0.0" - "kubernetes >= 12.0.0"
- "PyYAML >= 3.11" - "PyYAML >= 3.11"
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Scale deployment up, and extend timeout - name: Scale deployment up, and extend timeout
kubernetes.core.k8s_scale: kubernetes.core.k8s_scale:
api_version: v1 api_version: v1
@@ -105,9 +105,9 @@ EXAMPLES = r'''
label_selectors: label_selectors:
- app=test - app=test
continue_on_error: true continue_on_error: true
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- If a change was made, will return the patched object, otherwise returns the existing object. - If a change was made, will return the patched object, otherwise returns the existing object.
@@ -139,133 +139,166 @@ result:
returned: when C(wait) is true returned: when C(wait) is true
type: int type: int
sample: 48 sample: 48
''' """
import copy import copy
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC) AUTH_ARG_SPEC,
RESOURCE_ARG_SPEC,
NAME_ARG_SPEC,
)
SCALE_ARG_SPEC = { SCALE_ARG_SPEC = {
'replicas': {'type': 'int', 'required': True}, "replicas": {"type": "int", "required": True},
'current_replicas': {'type': 'int'}, "current_replicas": {"type": "int"},
'resource_version': {}, "resource_version": {},
'wait': {'type': 'bool', 'default': True}, "wait": {"type": "bool", "default": True},
'wait_timeout': {'type': 'int', 'default': 20}, "wait_timeout": {"type": "int", "default": 20},
'wait_sleep': {'type': 'int', 'default': 5}, "wait_sleep": {"type": "int", "default": 5},
} }
def execute_module(module, k8s_ansible_mixin,): def execute_module(
module, k8s_ansible_mixin,
):
k8s_ansible_mixin.set_resource_definitions(module) k8s_ansible_mixin.set_resource_definitions(module)
definition = k8s_ansible_mixin.resource_definitions[0] definition = k8s_ansible_mixin.resource_definitions[0]
name = definition['metadata']['name'] name = definition["metadata"]["name"]
namespace = definition['metadata'].get('namespace') namespace = definition["metadata"].get("namespace")
api_version = definition['apiVersion'] api_version = definition["apiVersion"]
kind = definition['kind'] kind = definition["kind"]
current_replicas = module.params.get('current_replicas') current_replicas = module.params.get("current_replicas")
replicas = module.params.get('replicas') replicas = module.params.get("replicas")
resource_version = module.params.get('resource_version') resource_version = module.params.get("resource_version")
label_selectors = module.params.get('label_selectors') label_selectors = module.params.get("label_selectors")
if not label_selectors: if not label_selectors:
label_selectors = [] label_selectors = []
continue_on_error = module.params.get('continue_on_error') continue_on_error = module.params.get("continue_on_error")
wait = module.params.get('wait') wait = module.params.get("wait")
wait_time = module.params.get('wait_timeout') wait_time = module.params.get("wait_timeout")
wait_sleep = module.params.get('wait_sleep') wait_sleep = module.params.get("wait_sleep")
existing = None existing = None
existing_count = None existing_count = None
return_attributes = dict(result=dict()) return_attributes = dict(result=dict())
if module._diff: if module._diff:
return_attributes['diff'] = dict() return_attributes["diff"] = dict()
if wait: if wait:
return_attributes['duration'] = 0 return_attributes["duration"] = 0
resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True) resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import NotFoundError from ansible_collections.kubernetes.core.plugins.module_utils.common import (
NotFoundError,
)
multiple_scale = False multiple_scale = False
try: try:
existing = resource.get(name=name, namespace=namespace, label_selector=','.join(label_selectors)) existing = resource.get(
if existing.kind.endswith('List'): name=name, namespace=namespace, label_selector=",".join(label_selectors)
)
if existing.kind.endswith("List"):
existing_items = existing.items existing_items = existing.items
multiple_scale = len(existing_items) > 1 multiple_scale = len(existing_items) > 1
else: else:
existing_items = [existing] existing_items = [existing]
except NotFoundError as exc: except NotFoundError as exc:
module.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc), module.fail_json(
error=exc.value.get('status')) msg="Failed to retrieve requested object: {0}".format(exc),
error=exc.value.get("status"),
)
if multiple_scale: if multiple_scale:
# when scaling multiple resource, the 'result' is changed to 'results' and is a list # when scaling multiple resource, the 'result' is changed to 'results' and is a list
return_attributes = {'results': []} return_attributes = {"results": []}
changed = False changed = False
def _continue_or_fail(error): def _continue_or_fail(error):
if multiple_scale and continue_on_error: if multiple_scale and continue_on_error:
if "errors" not in return_attributes: if "errors" not in return_attributes:
return_attributes['errors'] = [] return_attributes["errors"] = []
return_attributes['errors'].append({'error': error, 'failed': True}) return_attributes["errors"].append({"error": error, "failed": True})
else: else:
module.fail_json(msg=error, **return_attributes) module.fail_json(msg=error, **return_attributes)
def _continue_or_exit(warn): def _continue_or_exit(warn):
if multiple_scale: if multiple_scale:
return_attributes['results'].append({'warning': warn, 'changed': False}) return_attributes["results"].append({"warning": warn, "changed": False})
else: else:
module.exit_json(warning=warn, **return_attributes) module.exit_json(warning=warn, **return_attributes)
for existing in existing_items: for existing in existing_items:
if module.params['kind'] == 'job': if module.params["kind"] == "job":
existing_count = existing.spec.parallelism existing_count = existing.spec.parallelism
elif hasattr(existing.spec, 'replicas'): elif hasattr(existing.spec, "replicas"):
existing_count = existing.spec.replicas existing_count = existing.spec.replicas
if existing_count is None: if existing_count is None:
error = 'Failed to retrieve the available count for object kind={0} name={1} namespace={2}.'.format( error = "Failed to retrieve the available count for object kind={0} name={1} namespace={2}.".format(
existing.kind, existing.metadata.name, existing.metadata.namespace) existing.kind, existing.metadata.name, existing.metadata.namespace
)
_continue_or_fail(error) _continue_or_fail(error)
continue continue
if resource_version and resource_version != existing.metadata.resourceVersion: if resource_version and resource_version != existing.metadata.resourceVersion:
warn = 'expected resource version {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.'.format( warn = "expected resource version {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
resource_version, existing.metadata.resourceVersion, existing.kind, existing.metadata.name, existing.metadata.namespace) resource_version,
existing.metadata.resourceVersion,
existing.kind,
existing.metadata.name,
existing.metadata.namespace,
)
_continue_or_exit(warn) _continue_or_exit(warn)
continue continue
if current_replicas is not None and existing_count != current_replicas: if current_replicas is not None and existing_count != current_replicas:
warn = 'current replicas {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.'.format( warn = "current replicas {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
current_replicas, existing_count, existing.kind, existing.metadata.name, existing.metadata.namespace) current_replicas,
existing_count,
existing.kind,
existing.metadata.name,
existing.metadata.namespace,
)
_continue_or_exit(warn) _continue_or_exit(warn)
continue continue
if existing_count != replicas: if existing_count != replicas:
if not module.check_mode: if not module.check_mode:
if module.params['kind'] == 'job': if module.params["kind"] == "job":
existing.spec.parallelism = replicas existing.spec.parallelism = replicas
result = resource.patch(existing.to_dict()).to_dict() result = resource.patch(existing.to_dict()).to_dict()
else: else:
result = scale(module, k8s_ansible_mixin, resource, existing, replicas, wait, wait_time, wait_sleep) result = scale(
changed = changed or result['changed'] module,
k8s_ansible_mixin,
resource,
existing,
replicas,
wait,
wait_time,
wait_sleep,
)
changed = changed or result["changed"]
else: else:
name = existing.metadata.name name = existing.metadata.name
namespace = existing.metadata.namespace namespace = existing.metadata.namespace
existing = resource.get(name=name, namespace=namespace) existing = resource.get(name=name, namespace=namespace)
result = {'changed': False, 'result': existing.to_dict()} result = {"changed": False, "result": existing.to_dict()}
if module._diff: if module._diff:
result['diff'] = {} result["diff"] = {}
if wait: if wait:
result['duration'] = 0 result["duration"] = 0
# append result to the return attribute # append result to the return attribute
if multiple_scale: if multiple_scale:
return_attributes['results'].append(result) return_attributes["results"].append(result)
else: else:
module.exit_json(**result) module.exit_json(**result)
@@ -277,22 +310,35 @@ def argspec():
args.update(RESOURCE_ARG_SPEC) args.update(RESOURCE_ARG_SPEC)
args.update(NAME_ARG_SPEC) args.update(NAME_ARG_SPEC)
args.update(AUTH_ARG_SPEC) args.update(AUTH_ARG_SPEC)
args.update({'label_selectors': {'type': 'list', 'elements': 'str', 'default': []}}) args.update({"label_selectors": {"type": "list", "elements": "str", "default": []}})
args.update(({'continue_on_error': {'type': 'bool', 'default': False}})) args.update(({"continue_on_error": {"type": "bool", "default": False}}))
return args return args
def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait, wait_time, wait_sleep): def scale(
module,
k8s_ansible_mixin,
resource,
existing_object,
replicas,
wait,
wait_time,
wait_sleep,
):
name = existing_object.metadata.name name = existing_object.metadata.name
namespace = existing_object.metadata.namespace namespace = existing_object.metadata.namespace
kind = existing_object.kind kind = existing_object.kind
if not hasattr(resource, 'scale'): if not hasattr(resource, "scale"):
module.fail_json( module.fail_json(
msg="Cannot perform scale on resource of kind {0}".format(resource.kind) msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
) )
scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}} scale_obj = {
"kind": kind,
"metadata": {"name": name, "namespace": namespace},
"spec": {"replicas": replicas},
}
existing = resource.get(name=name, namespace=namespace) existing = resource.get(name=name, namespace=namespace)
@@ -304,13 +350,15 @@ def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait,
k8s_obj = resource.get(name=name, namespace=namespace).to_dict() k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj) match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
result = dict() result = dict()
result['result'] = k8s_obj result["result"] = k8s_obj
result['changed'] = not match result["changed"] = not match
if module._diff: if module._diff:
result['diff'] = diffs result["diff"] = diffs
if wait: if wait:
success, result['result'], result['duration'] = k8s_ansible_mixin.wait(resource, scale_obj, wait_sleep, wait_time) success, result["result"], result["duration"] = k8s_ansible_mixin.wait(
resource, scale_obj, wait_sleep, wait_time
)
if not success: if not success:
module.fail_json(msg="Resource scaling timed out", **result) module.fail_json(msg="Resource scaling timed out", **result)
return result return result
@@ -318,15 +366,22 @@ def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait,
def main(): def main():
mutually_exclusive = [ mutually_exclusive = [
('resource_definition', 'src'), ("resource_definition", "src"),
] ]
module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True) module = AnsibleModule(
argument_spec=argspec(),
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r"""
module: k8s_service module: k8s_service
@@ -85,9 +85,9 @@ options:
requirements: requirements:
- python >= 3.6 - python >= 3.6
- kubernetes >= 12.0.0 - kubernetes >= 12.0.0
''' """
EXAMPLES = r''' EXAMPLES = r"""
- name: Expose https port with ClusterIP - name: Expose https port with ClusterIP
kubernetes.core.k8s_service: kubernetes.core.k8s_service:
state: present state: present
@@ -111,9 +111,9 @@ EXAMPLES = r'''
protocol: TCP protocol: TCP
selector: selector:
key: special key: special
''' """
RETURN = r''' RETURN = r"""
result: result:
description: description:
- The created, patched, or otherwise present Service object. Will be empty in the case of a deletion. - The created, patched, or otherwise present Service object. Will be empty in the case of a deletion.
@@ -140,32 +140,36 @@ result:
description: Current status details for the object. description: Current status details for the object.
returned: success returned: success
type: complex type: complex
''' """
import copy import copy
from collections import defaultdict from collections import defaultdict
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC) AUTH_ARG_SPEC,
COMMON_ARG_SPEC,
RESOURCE_ARG_SPEC,
)
SERVICE_ARG_SPEC = { SERVICE_ARG_SPEC = {
'apply': { "apply": {"type": "bool", "default": False},
'type': 'bool', "name": {"required": True},
'default': False, "namespace": {"required": True},
"merge_type": {
"type": "list",
"elements": "str",
"choices": ["json", "merge", "strategic-merge"],
}, },
'name': {'required': True}, "selector": {"type": "dict"},
'namespace': {'required': True}, "type": {
'merge_type': {'type': 'list', 'elements': 'str', 'choices': ['json', 'merge', 'strategic-merge']}, "type": "str",
'selector': {'type': 'dict'}, "choices": ["NodePort", "ClusterIP", "LoadBalancer", "ExternalName"],
'type': {
'type': 'str',
'choices': [
'NodePort', 'ClusterIP', 'LoadBalancer', 'ExternalName'
],
}, },
'ports': {'type': 'list', 'elements': 'dict'}, "ports": {"type": "list", "elements": "dict"},
} }
@@ -195,29 +199,31 @@ def execute_module(module, k8s_ansible_mixin):
""" Module execution """ """ Module execution """
k8s_ansible_mixin.set_resource_definitions(module) k8s_ansible_mixin.set_resource_definitions(module)
api_version = 'v1' api_version = "v1"
selector = module.params.get('selector') selector = module.params.get("selector")
service_type = module.params.get('type') service_type = module.params.get("type")
ports = module.params.get('ports') ports = module.params.get("ports")
definition = defaultdict(defaultdict) definition = defaultdict(defaultdict)
definition['kind'] = 'Service' definition["kind"] = "Service"
definition['apiVersion'] = api_version definition["apiVersion"] = api_version
def_spec = definition['spec'] def_spec = definition["spec"]
def_spec['type'] = service_type def_spec["type"] = service_type
def_spec['ports'] = ports def_spec["ports"] = ports
def_spec['selector'] = selector def_spec["selector"] = selector
def_meta = definition['metadata'] def_meta = definition["metadata"]
def_meta['name'] = module.params.get('name') def_meta["name"] = module.params.get("name")
def_meta['namespace'] = module.params.get('namespace') def_meta["namespace"] = module.params.get("namespace")
# 'resource_definition:' has lower priority than module parameters # 'resource_definition:' has lower priority than module parameters
definition = dict(merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition)) definition = dict(
merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition)
)
resource = k8s_ansible_mixin.find_resource('Service', api_version, fail=True) resource = k8s_ansible_mixin.find_resource("Service", api_version, fail=True)
definition = k8s_ansible_mixin.set_defaults(resource, definition) definition = k8s_ansible_mixin.set_defaults(resource, definition)
result = k8s_ansible_mixin.perform_action(resource, definition) result = k8s_ansible_mixin.perform_action(resource, definition)
@@ -227,12 +233,14 @@ def execute_module(module, k8s_ansible_mixin):
def main(): def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client) K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module) k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin) execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -6,10 +6,11 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = """
--- ---
module: test_tempfile module: test_tempfile
@@ -50,7 +51,7 @@ seealso:
author: author:
- Krzysztof Magosa (@krzysztof-magosa) - Krzysztof Magosa (@krzysztof-magosa)
''' """
EXAMPLES = """ EXAMPLES = """
- name: create temporary build directory - name: create temporary build directory
@@ -71,13 +72,13 @@ EXAMPLES = """
when: tempfile_1.path is defined when: tempfile_1.path is defined
""" """
RETURN = ''' RETURN = """
path: path:
description: Path to created file or directory description: Path to created file or directory
returned: success returned: success
type: str type: str
sample: "/tmp/ansible.bMlvdk" sample: "/tmp/ansible.bMlvdk"
''' """
from os import close from os import close
from tempfile import mkstemp, mkdtemp from tempfile import mkstemp, mkdtemp
@@ -90,26 +91,26 @@ from ansible.module_utils._text import to_native
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
state=dict(type='str', default='file', choices=['file', 'directory']), state=dict(type="str", default="file", choices=["file", "directory"]),
path=dict(type='path'), path=dict(type="path"),
prefix=dict(type='str', default='ansible.'), prefix=dict(type="str", default="ansible."),
suffix=dict(type='str', default=''), suffix=dict(type="str", default=""),
), ),
) )
try: try:
if module.params['state'] == 'file': if module.params["state"] == "file":
handle, path = mkstemp( handle, path = mkstemp(
prefix=module.params['prefix'], prefix=module.params["prefix"],
suffix=module.params['suffix'], suffix=module.params["suffix"],
dir=module.params['path'], dir=module.params["path"],
) )
close(handle) close(handle)
elif module.params['state'] == 'directory': elif module.params["state"] == "directory":
path = mkdtemp( path = mkdtemp(
prefix=module.params['prefix'], prefix=module.params["prefix"],
suffix=module.params['suffix'], suffix=module.params["suffix"],
dir=module.params['path'], dir=module.params["path"],
) )
module.exit_json(changed=True, path=path) module.exit_json(changed=True, path=path)
@@ -117,5 +118,5 @@ def main():
module.fail_json(msg=to_native(e), exception=format_exc()) module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -14,408 +14,477 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible_collections.kubernetes.core.plugins.module_utils.apply import merge, apply_patch from ansible_collections.kubernetes.core.plugins.module_utils.apply import (
merge,
apply_patch,
)
tests = [ tests = [
dict( dict(
last_applied=dict( last_applied=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2")
metadata=dict(name="foo"),
data=dict(one="1", two="2")
), ),
desired=dict( desired=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2")
metadata=dict(name="foo"),
data=dict(one="1", two="2")
), ),
expected={} expected={},
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2")
metadata=dict(name="foo"),
data=dict(one="1", two="2")
), ),
desired=dict( desired=dict(
kind="ConfigMap", kind="ConfigMap",
metadata=dict(name="foo"), metadata=dict(name="foo"),
data=dict(one="1", two="2", three="3") data=dict(one="1", two="2", three="3"),
), ),
expected=dict(data=dict(three="3")) expected=dict(data=dict(three="3")),
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2")
metadata=dict(name="foo"),
data=dict(one="1", two="2")
), ),
desired=dict( desired=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3")
metadata=dict(name="foo"),
data=dict(one="1", three="3")
), ),
expected=dict(data=dict(two=None, three="3")) expected=dict(data=dict(two=None, three="3")),
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="ConfigMap", kind="ConfigMap",
metadata=dict(name="foo", annotations=dict(this="one", hello="world")), metadata=dict(name="foo", annotations=dict(this="one", hello="world")),
data=dict(one="1", two="2") data=dict(one="1", two="2"),
), ),
desired=dict( desired=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3")
metadata=dict(name="foo"),
data=dict(one="1", three="3")
), ),
expected=dict(metadata=dict(annotations=None), data=dict(two=None, three="3")) expected=dict(metadata=dict(annotations=None), data=dict(two=None, three="3")),
),
dict(
last_applied=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, name="http")])
),
actual=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")])
),
desired=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, name="http")])
),
expected=dict(spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]))
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, name="http")]) spec=dict(ports=[dict(port=8080, name="http")]),
), ),
actual=dict( actual=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) spec=dict(ports=[dict(port=8080, protocol="TCP", name="http")]),
), ),
desired=dict( desired=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8081, name="http")]) spec=dict(ports=[dict(port=8080, name="http")]),
), ),
expected=dict(spec=dict(ports=[dict(port=8081, name="http")])) expected=dict(spec=dict(ports=[dict(port=8080, protocol="TCP", name="http")])),
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, name="http")]) spec=dict(ports=[dict(port=8080, name="http")]),
), ),
actual=dict( actual=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) spec=dict(ports=[dict(port=8080, protocol="TCP", name="http")]),
), ),
desired=dict( desired=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) spec=dict(ports=[dict(port=8081, name="http")]),
), ),
expected=dict(spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http", protocol='TCP')])) expected=dict(spec=dict(ports=[dict(port=8081, name="http")])),
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) spec=dict(ports=[dict(port=8080, name="http")]),
), ),
actual=dict( actual=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, protocol='TCP', name="https"), dict(port=8080, protocol='TCP', name='http')]) spec=dict(ports=[dict(port=8080, protocol="TCP", name="http")]),
), ),
desired=dict( desired=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8080, name="http")]) spec=dict(
ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]
),
),
expected=dict(
spec=dict(
ports=[
dict(port=8443, name="https"),
dict(port=8080, name="http", protocol="TCP"),
]
)
), ),
expected=dict(spec=dict(ports=[dict(port=8080, name="http", protocol='TCP')]))
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, name="https", madeup="xyz"), dict(port=8080, name="http")]) spec=dict(
ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]
),
), ),
actual=dict( actual=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, protocol='TCP', name="https", madeup="xyz"), dict(port=8080, protocol='TCP', name='http')]) spec=dict(
ports=[
dict(port=8443, protocol="TCP", name="https"),
dict(port=8080, protocol="TCP", name="http"),
]
),
), ),
desired=dict( desired=dict(
kind="Service", kind="Service",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, name="https")]) spec=dict(ports=[dict(port=8080, name="http")]),
),
expected=dict(spec=dict(ports=[dict(port=8080, name="http", protocol="TCP")])),
),
dict(
last_applied=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(
ports=[
dict(port=8443, name="https", madeup="xyz"),
dict(port=8080, name="http"),
]
),
),
actual=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(
ports=[
dict(port=8443, protocol="TCP", name="https", madeup="xyz"),
dict(port=8080, protocol="TCP", name="http"),
]
),
),
desired=dict(
kind="Service",
metadata=dict(name="foo"),
spec=dict(ports=[dict(port=8443, name="https")]),
),
expected=dict(
spec=dict(
ports=[dict(madeup=None, port=8443, name="https", protocol="TCP")]
)
), ),
expected=dict(spec=dict(ports=[dict(madeup=None, port=8443, name="https", protocol='TCP')]))
), ),
dict( dict(
last_applied=dict( last_applied=dict(
kind="Pod", kind="Pod",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(containers=[dict(name="busybox", image="busybox", spec=dict(
resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) containers=[
dict(
name="busybox",
image="busybox",
resources=dict(
requests=dict(cpu="100m", memory="100Mi"),
limits=dict(cpu="100m", memory="100Mi"),
),
)
]
),
), ),
actual=dict( actual=dict(
kind="Pod", kind="Pod",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(containers=[dict(name="busybox", image="busybox", spec=dict(
resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) containers=[
dict(
name="busybox",
image="busybox",
resources=dict(
requests=dict(cpu="100m", memory="100Mi"),
limits=dict(cpu="100m", memory="100Mi"),
),
)
]
),
), ),
desired=dict( desired=dict(
kind="Pod", kind="Pod",
metadata=dict(name="foo"), metadata=dict(name="foo"),
spec=dict(containers=[dict(name="busybox", image="busybox", spec=dict(
resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(memory="50Mi")))]) containers=[
dict(
name="busybox",
image="busybox",
resources=dict(
requests=dict(cpu="50m", memory="50Mi"),
limits=dict(memory="50Mi"),
),
)
]
),
),
expected=dict(
spec=dict(
containers=[
dict(
name="busybox",
image="busybox",
resources=dict(
requests=dict(cpu="50m", memory="50Mi"),
limits=dict(cpu=None, memory="50Mi"),
),
)
]
)
), ),
expected=dict(spec=dict(containers=[dict(name="busybox", image="busybox",
resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(cpu=None, memory="50Mi")))]))
), ),
dict( dict(
desired=dict(kind='Pod', desired=dict(
spec=dict(containers=[ kind="Pod",
dict(name='hello', spec=dict(
volumeMounts=[dict(name="test", mountPath="/test")]) containers=[
], dict(
volumes=[ name="hello",
dict(name="test", configMap=dict(name="test")), volumeMounts=[dict(name="test", mountPath="/test")],
])), )
last_applied=dict(kind='Pod', ],
spec=dict(containers=[ volumes=[dict(name="test", configMap=dict(name="test"))],
dict(name='hello', ),
volumeMounts=[dict(name="test", mountPath="/test")]) ),
], last_applied=dict(
volumes=[ kind="Pod",
dict(name="test", configMap=dict(name="test"))])), spec=dict(
actual=dict(kind='Pod', containers=[
spec=dict(containers=[ dict(
dict(name='hello', name="hello",
volumeMounts=[ volumeMounts=[dict(name="test", mountPath="/test")],
dict(name="test", mountPath="/test"), )
dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")]) ],
], volumes=[dict(name="test", configMap=dict(name="test"))],
volumes=[ ),
dict(name="test", configMap=dict(name="test")), ),
dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz")), actual=dict(
])), kind="Pod",
expected=dict(spec=dict(containers=[dict(name='hello', spec=dict(
volumeMounts=[dict(name="test", mountPath="/test"), containers=[
dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")])], dict(
volumes=[dict(name="test", configMap=dict(name="test")), name="hello",
dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz"))])), volumeMounts=[
dict(name="test", mountPath="/test"),
dict(
mountPath="/var/run/secrets/kubernetes.io/serviceaccount",
name="default-token-xyz",
),
],
)
],
volumes=[
dict(name="test", configMap=dict(name="test")),
dict(
name="default-token-xyz",
secret=dict(secretName="default-token-xyz"),
),
],
),
),
expected=dict(
spec=dict(
containers=[
dict(
name="hello",
volumeMounts=[
dict(name="test", mountPath="/test"),
dict(
mountPath="/var/run/secrets/kubernetes.io/serviceaccount",
name="default-token-xyz",
),
],
)
],
volumes=[
dict(name="test", configMap=dict(name="test")),
dict(
name="default-token-xyz",
secret=dict(secretName="default-token-xyz"),
),
],
)
),
), ),
# This next one is based on a real world case where definition was mostly # This next one is based on a real world case where definition was mostly
# str type and everything else was mostly unicode type (don't ask me how) # str type and everything else was mostly unicode type (don't ask me how)
dict( dict(
last_applied={ last_applied={
u'kind': u'ConfigMap', u"kind": u"ConfigMap",
u'data': {u'one': '1', 'three': '3', 'two': '2'}, u"data": {u"one": "1", "three": "3", "two": "2"},
u'apiVersion': u'v1', u"apiVersion": u"v1",
u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap'} u"metadata": {u"namespace": u"apply", u"name": u"apply-configmap"},
}, },
actual={ actual={
u'kind': u'ConfigMap', u"kind": u"ConfigMap",
u'data': {u'one': '1', 'three': '3', 'two': '2'}, u"data": {u"one": "1", "three": "3", "two": "2"},
u'apiVersion': u'v1', u"apiVersion": u"v1",
u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap', u"metadata": {
u'resourceVersion': '1714994', u"namespace": u"apply",
u'creationTimestamp': u'2019-08-17T05:08:05Z', u'annotations': {}, u"name": u"apply-configmap",
u'selfLink': u'/api/v1/namespaces/apply/configmaps/apply-configmap', u"resourceVersion": "1714994",
u'uid': u'fed45fb0-c0ac-11e9-9d95-025000000001'} u"creationTimestamp": u"2019-08-17T05:08:05Z",
u"annotations": {},
u"selfLink": u"/api/v1/namespaces/apply/configmaps/apply-configmap",
u"uid": u"fed45fb0-c0ac-11e9-9d95-025000000001",
},
}, },
desired={ desired={
'kind': u'ConfigMap', "kind": u"ConfigMap",
'data': {'one': '1', 'three': '3', 'two': '2'}, "data": {"one": "1", "three": "3", "two": "2"},
'apiVersion': 'v1', "apiVersion": "v1",
'metadata': {'namespace': 'apply', 'name': 'apply-configmap'} "metadata": {"namespace": "apply", "name": "apply-configmap"},
}, },
expected=dict() expected=dict(),
), ),
# apply a Deployment, then scale the Deployment (which doesn't affect last-applied) # apply a Deployment, then scale the Deployment (which doesn't affect last-applied)
# then apply the Deployment again. Should un-scale the Deployment # then apply the Deployment again. Should un-scale the Deployment
dict( dict(
last_applied={ last_applied={
'kind': u'Deployment', "kind": u"Deployment",
'spec': { "spec": {
'replicas': 1, "replicas": 1,
'template': { "template": {
'spec': { "spec": {
'containers': [ "containers": [
{ {
'name': 'this_must_exist', "name": "this_must_exist",
'envFrom': [ "envFrom": [
{ {"configMapRef": {"name": "config-xyz"}},
'configMapRef': { {"secretRef": {"name": "config-wxy"}},
'name': 'config-xyz' ],
}
},
{
'secretRef': {
'name': 'config-wxy'
}
}
]
} }
] ]
} }
} },
}, },
'metadata': { "metadata": {"namespace": "apply", "name": u"apply-deployment"},
'namespace': 'apply',
'name': u'apply-deployment'
}
}, },
actual={ actual={
'kind': u'Deployment', "kind": u"Deployment",
'spec': { "spec": {
'replicas': 0, "replicas": 0,
'template': { "template": {
'spec': { "spec": {
'containers': [ "containers": [
{ {
'name': 'this_must_exist', "name": "this_must_exist",
'envFrom': [ "envFrom": [
{ {"configMapRef": {"name": "config-xyz"}},
'configMapRef': { {"secretRef": {"name": "config-wxy"}},
'name': 'config-xyz' ],
}
},
{
'secretRef': {
'name': 'config-wxy'
}
}
]
} }
] ]
} }
} },
}, },
'metadata': { "metadata": {"namespace": "apply", "name": u"apply-deployment"},
'namespace': 'apply',
'name': u'apply-deployment'
}
}, },
desired={ desired={
'kind': u'Deployment', "kind": u"Deployment",
'spec': { "spec": {
'replicas': 1, "replicas": 1,
'template': { "template": {
'spec': { "spec": {
'containers': [ "containers": [
{ {
'name': 'this_must_exist', "name": "this_must_exist",
'envFrom': [ "envFrom": [{"configMapRef": {"name": "config-abc"}}],
{
'configMapRef': {
'name': 'config-abc'
}
}
]
} }
] ]
} }
} },
}, },
'metadata': { "metadata": {"namespace": "apply", "name": u"apply-deployment"},
'namespace': 'apply',
'name': u'apply-deployment'
}
}, },
expected={ expected={
'spec': { "spec": {
'replicas': 1, "replicas": 1,
'template': { "template": {
'spec': { "spec": {
'containers': [ "containers": [
{ {
'name': 'this_must_exist', "name": "this_must_exist",
'envFrom': [ "envFrom": [{"configMapRef": {"name": "config-abc"}}],
{
'configMapRef': {
'name': 'config-abc'
}
}
]
} }
] ]
} }
} },
} }
} },
), ),
dict( dict(
last_applied={ last_applied={"kind": "MadeUp", "toplevel": {"original": "entry"}},
'kind': 'MadeUp',
'toplevel': {
'original': 'entry'
}
},
actual={ actual={
'kind': 'MadeUp', "kind": "MadeUp",
'toplevel': { "toplevel": {
'original': 'entry', "original": "entry",
'another': { "another": {"nested": {"entry": "value"}},
'nested': { },
'entry': 'value'
}
}
}
}, },
desired={ desired={
'kind': 'MadeUp', "kind": "MadeUp",
'toplevel': { "toplevel": {
'original': 'entry', "original": "entry",
'another': { "another": {"nested": {"entry": "value"}},
'nested': { },
'entry': 'value'
}
}
}
}, },
expected={} expected={},
) ),
] ]
def test_merges(): def test_merges():
for test in tests: for test in tests:
assert(merge(test['last_applied'], test['desired'], test.get('actual', test['last_applied'])) == test['expected']) assert (
merge(
test["last_applied"],
test["desired"],
test.get("actual", test["last_applied"]),
)
== test["expected"]
)
def test_apply_patch(): def test_apply_patch():
actual = dict( actual = dict(
kind="ConfigMap", kind="ConfigMap",
metadata=dict(name="foo", metadata=dict(
annotations={'kubectl.kubernetes.io/last-applied-configuration': name="foo",
'{"data":{"one":"1","two":"2"},"kind":"ConfigMap",' annotations={
'"metadata":{"annotations":{"hello":"world","this":"one"},"name":"foo"}}', "kubectl.kubernetes.io/last-applied-configuration": '{"data":{"one":"1","two":"2"},"kind":"ConfigMap",'
'this': 'one', 'hello': 'world'}), '"metadata":{"annotations":{"hello":"world","this":"one"},"name":"foo"}}',
data=dict(one="1", two="2") "this": "one",
"hello": "world",
},
),
data=dict(one="1", two="2"),
) )
desired = dict( desired = dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3")
metadata=dict(name="foo"),
data=dict(one="1", three="3")
) )
expected = dict( expected = dict(
metadata=dict( metadata=dict(
annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","three":"3"},"kind":"ConfigMap","metadata":{"name":"foo"}}', annotations={
'this': None, 'hello': None}), "kubectl.kubernetes.io/last-applied-configuration": '{"data":{"one":"1","three":"3"},"kind":"ConfigMap","metadata":{"name":"foo"}}',
data=dict(two=None, three="3") "this": None,
"hello": None,
}
),
data=dict(two=None, three="3"),
) )
assert(apply_patch(actual, desired) == (actual, expected)) assert apply_patch(actual, desired) == (actual, expected)

View File

@@ -17,9 +17,7 @@ def test_encode_stringdata_modifies_definition():
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Secret", "kind": "Secret",
"type": "Opaque", "type": "Opaque",
"stringData": { "stringData": {"mydata": "ansiβle"},
"mydata": "ansiβle"
}
} }
res = _encode_stringdata(definition) res = _encode_stringdata(definition)
assert "stringData" not in res assert "stringData" not in res
@@ -31,9 +29,7 @@ def test_encode_stringdata_does_not_modify_data():
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Secret", "kind": "Secret",
"type": "Opaque", "type": "Opaque",
"data": { "data": {"mydata": "Zm9vYmFy"},
"mydata": "Zm9vYmFy"
}
} }
res = _encode_stringdata(definition) res = _encode_stringdata(definition)
assert res["data"]["mydata"] == "Zm9vYmFy" assert res["data"]["mydata"] == "Zm9vYmFy"

View File

@@ -18,85 +18,101 @@ import pytest
from kubernetes.client import ApiClient from kubernetes.client import ApiClient
from kubernetes.dynamic import Resource from kubernetes.dynamic import Resource
from ansible_collections.kubernetes.core.plugins.module_utils.k8sdynamicclient import K8SDynamicClient from ansible_collections.kubernetes.core.plugins.module_utils.k8sdynamicclient import (
from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import LazyDiscoverer K8SDynamicClient,
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList )
from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import (
LazyDiscoverer,
)
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
ResourceList,
)
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def mock_namespace(): def mock_namespace():
return Resource( return Resource(
api_version='v1', api_version="v1",
kind='Namespace', kind="Namespace",
name='namespaces', name="namespaces",
namespaced=False, namespaced=False,
preferred=True, preferred=True,
prefix='api', prefix="api",
shorter_names=['ns'], shorter_names=["ns"],
shortNames=['ns'], shortNames=["ns"],
singularName='namespace', singularName="namespace",
verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] verbs=["create", "delete", "get", "list", "patch", "update", "watch"],
) )
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def mock_templates(): def mock_templates():
return Resource( return Resource(
api_version='v1', api_version="v1",
kind='Template', kind="Template",
name='templates', name="templates",
namespaced=True, namespaced=True,
preferred=True, preferred=True,
prefix='api', prefix="api",
shorter_names=[], shorter_names=[],
shortNames=[], shortNames=[],
verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] verbs=["create", "delete", "get", "list", "patch", "update", "watch"],
) )
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def mock_processedtemplates(): def mock_processedtemplates():
return Resource( return Resource(
api_version='v1', api_version="v1",
kind='Template', kind="Template",
name='processedtemplates', name="processedtemplates",
namespaced=True, namespaced=True,
preferred=True, preferred=True,
prefix='api', prefix="api",
shorter_names=[], shorter_names=[],
shortNames=[], shortNames=[],
verbs=['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] verbs=["create", "delete", "get", "list", "patch", "update", "watch"],
) )
@pytest.fixture(scope='module') @pytest.fixture(scope="module")
def mock_namespace_list(mock_namespace): def mock_namespace_list(mock_namespace):
ret = ResourceList(mock_namespace.client, mock_namespace.group, mock_namespace.api_version, mock_namespace.kind) ret = ResourceList(
mock_namespace.client,
mock_namespace.group,
mock_namespace.api_version,
mock_namespace.kind,
)
ret._ResourceList__base_resource = mock_namespace ret._ResourceList__base_resource = mock_namespace
return ret return ret
@pytest.fixture(scope='function', autouse=True) @pytest.fixture(scope="function", autouse=True)
def setup_client_monkeypatch(monkeypatch, mock_namespace, mock_namespace_list, mock_templates, mock_processedtemplates): def setup_client_monkeypatch(
monkeypatch,
mock_namespace,
mock_namespace_list,
mock_templates,
mock_processedtemplates,
):
def mock_load_server_info(self): def mock_load_server_info(self):
self.__version = {'kubernetes': 'mock-k8s-version'} self.__version = {"kubernetes": "mock-k8s-version"}
def mock_parse_api_groups(self, request_resources=False): def mock_parse_api_groups(self, request_resources=False):
return { return {
'api': { "api": {
'': { "": {
'v1': { "v1": {
'Namespace': [mock_namespace], "Namespace": [mock_namespace],
'NamespaceList': [mock_namespace_list], "NamespaceList": [mock_namespace_list],
'Template': [mock_templates, mock_processedtemplates], "Template": [mock_templates, mock_processedtemplates],
} }
} }
} }
} }
monkeypatch.setattr(LazyDiscoverer, '_load_server_info', mock_load_server_info) monkeypatch.setattr(LazyDiscoverer, "_load_server_info", mock_load_server_info)
monkeypatch.setattr(LazyDiscoverer, 'parse_api_groups', mock_parse_api_groups) monkeypatch.setattr(LazyDiscoverer, "parse_api_groups", mock_parse_api_groups)
@pytest.fixture @pytest.fixture
@@ -104,39 +120,45 @@ def client(request):
return K8SDynamicClient(ApiClient(), discoverer=LazyDiscoverer) return K8SDynamicClient(ApiClient(), discoverer=LazyDiscoverer)
@pytest.mark.parametrize(("attribute", "value"), [ @pytest.mark.parametrize(
('name', 'namespaces'), ("attribute", "value"),
('singular_name', 'namespace'), [("name", "namespaces"), ("singular_name", "namespace"), ("short_names", ["ns"])],
('short_names', ['ns']) )
]) def test_search_returns_single_and_list(
def test_search_returns_single_and_list(client, mock_namespace, mock_namespace_list, attribute, value): client, mock_namespace, mock_namespace_list, attribute, value
resources = client.resources.search(**{'api_version': 'v1', attribute: value}) ):
resources = client.resources.search(**{"api_version": "v1", attribute: value})
assert len(resources) == 2 assert len(resources) == 2
assert mock_namespace in resources assert mock_namespace in resources
assert mock_namespace_list in resources assert mock_namespace_list in resources
@pytest.mark.parametrize(("attribute", "value"), [ @pytest.mark.parametrize(
('kind', 'Namespace'), ("attribute", "value"),
('name', 'namespaces'), [
('singular_name', 'namespace'), ("kind", "Namespace"),
('short_names', ['ns']) ("name", "namespaces"),
]) ("singular_name", "namespace"),
("short_names", ["ns"]),
],
)
def test_get_returns_only_single(client, mock_namespace, attribute, value): def test_get_returns_only_single(client, mock_namespace, attribute, value):
resource = client.resources.get(**{'api_version': 'v1', attribute: value}) resource = client.resources.get(**{"api_version": "v1", attribute: value})
assert resource == mock_namespace assert resource == mock_namespace
def test_get_namespace_list_kind(client, mock_namespace_list): def test_get_namespace_list_kind(client, mock_namespace_list):
resource = client.resources.get(api_version='v1', kind='NamespaceList') resource = client.resources.get(api_version="v1", kind="NamespaceList")
assert resource == mock_namespace_list assert resource == mock_namespace_list
def test_search_multiple_resources_for_template(client, mock_templates, mock_processedtemplates): def test_search_multiple_resources_for_template(
resources = client.resources.search(api_version='v1', kind='Template') client, mock_templates, mock_processedtemplates
):
resources = client.resources.search(api_version="v1", kind="Template")
assert len(resources) == 2 assert len(resources) == 2
assert mock_templates in resources assert mock_templates in resources

View File

@@ -15,17 +15,22 @@
# Test ConfigMapHash and SecretHash equivalents # Test ConfigMapHash and SecretHash equivalents
# tests based on https://github.com/kubernetes/kubernetes/pull/49961 # tests based on https://github.com/kubernetes/kubernetes/pull/49961
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
generate_hash,
)
tests = [ tests = [
dict(
resource=dict(kind="ConfigMap", metadata=dict(name="foo"), data=dict()),
expected="867km9574f",
),
dict( dict(
resource=dict( resource=dict(
kind="ConfigMap", kind="ConfigMap", metadata=dict(name="foo"), type="my-type", data=dict()
metadata=dict(name="foo"),
data=dict()
), ),
expected="867km9574f", expected="867km9574f",
), ),
@@ -33,53 +38,31 @@ tests = [
resource=dict( resource=dict(
kind="ConfigMap", kind="ConfigMap",
metadata=dict(name="foo"), metadata=dict(name="foo"),
type="my-type", data=dict(key1="value1", key2="value2"),
data=dict()
),
expected="867km9574f",
),
dict(
resource=dict(
kind="ConfigMap",
metadata=dict(name="foo"),
data=dict(
key1="value1",
key2="value2")
), ),
expected="gcb75dd9gb", expected="gcb75dd9gb",
), ),
dict( dict(
resource=dict( resource=dict(kind="Secret", metadata=dict(name="foo"), data=dict()),
kind="Secret",
metadata=dict(name="foo"),
data=dict()
),
expected="949tdgdkgg", expected="949tdgdkgg",
), ),
dict( dict(
resource=dict( resource=dict(
kind="Secret", kind="Secret", metadata=dict(name="foo"), type="my-type", data=dict()
metadata=dict(name="foo"),
type="my-type",
data=dict()
), ),
expected="dg474f9t76", expected="dg474f9t76",
), ),
dict( dict(
resource=dict( resource=dict(
kind="Secret", kind="Secret",
metadata=dict(name="foo"), metadata=dict(name="foo"),
data=dict( data=dict(key1="dmFsdWUx", key2="dmFsdWUy"),
key1="dmFsdWUx",
key2="dmFsdWUy")
), ),
expected="tf72c228m4", expected="tf72c228m4",
) ),
] ]
def test_hashes(): def test_hashes():
for test in tests: for test in tests:
assert(generate_hash(test['resource']) == test['expected']) assert generate_hash(test["resource"]) == test["expected"]

View File

@@ -15,78 +15,55 @@
# Test ConfigMap and Secret marshalling # Test ConfigMap and Secret marshalling
# tests based on https://github.com/kubernetes/kubernetes/pull/49961 # tests based on https://github.com/kubernetes/kubernetes/pull/49961
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import marshal, sorted_dict from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
marshal,
sorted_dict,
)
tests = [ tests = [
dict( dict(
resource=dict( resource=dict(kind="ConfigMap", name="", data=dict(),),
kind="ConfigMap", expected=b'{"data":{},"kind":"ConfigMap","name":""}',
name="", ),
data=dict(), dict(
), resource=dict(kind="ConfigMap", name="", data=dict(one=""),),
expected=b'{"data":{},"kind":"ConfigMap","name":""}' expected=b'{"data":{"one":""},"kind":"ConfigMap","name":""}',
), ),
dict( dict(
resource=dict( resource=dict(
kind="ConfigMap", kind="ConfigMap", name="", data=dict(two="2", one="", three="3",),
name="",
data=dict(
one=""
),
), ),
expected=b'{"data":{"one":""},"kind":"ConfigMap","name":""}' expected=b'{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}',
), ),
dict( dict(
resource=dict( resource=dict(kind="Secret", type="my-type", name="", data=dict(),),
kind="ConfigMap", expected=b'{"data":{},"kind":"Secret","name":"","type":"my-type"}',
name="", ),
data=dict( dict(
two="2", resource=dict(kind="Secret", type="my-type", name="", data=dict(one=""),),
one="", expected=b'{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}',
three="3",
),
),
expected=b'{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}'
), ),
dict( dict(
resource=dict( resource=dict(
kind="Secret", kind="Secret",
type="my-type", type="my-type",
name="", name="",
data=dict(), data=dict(two="Mg==", one="", three="Mw==",),
), ),
expected=b'{"data":{},"kind":"Secret","name":"","type":"my-type"}' expected=b'{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}',
),
dict(
resource=dict(
kind="Secret",
type="my-type",
name="",
data=dict(
one=""
),
),
expected=b'{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}'
),
dict(
resource=dict(
kind="Secret",
type="my-type",
name="",
data=dict(
two="Mg==",
one="",
three="Mw==",
),
),
expected=b'{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}'
), ),
] ]
def test_marshal(): def test_marshal():
for test in tests: for test in tests:
assert(marshal(sorted_dict(test['resource']), sorted(list(test['resource'].keys()))) == test['expected']) assert (
marshal(
sorted_dict(test["resource"]), sorted(list(test["resource"].keys()))
)
== test["expected"]
)

View File

@@ -12,54 +12,57 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from ansible_collections.kubernetes.core.plugins.module_utils.selector import LabelSelectorFilter, Selector from ansible_collections.kubernetes.core.plugins.module_utils.selector import (
LabelSelectorFilter,
Selector,
)
prod_definition = { prod_definition = {
'apiVersion': 'v1', "apiVersion": "v1",
'kind': 'Pod', "kind": "Pod",
'metadata': { "metadata": {
'name': 'test', "name": "test",
'labels': { "labels": {"environment": "production", "app": "nginx"},
'environment': 'production',
'app': 'nginx',
}
}, },
'spec': { "spec": {
'containers': [ "containers": [
{'name': 'nginx', 'image': 'nginx:1.14.2', 'command': ['/bin/sh', '-c', 'sleep 10']} {
"name": "nginx",
"image": "nginx:1.14.2",
"command": ["/bin/sh", "-c", "sleep 10"],
}
] ]
} },
} }
no_label_definition = { no_label_definition = {
'apiVersion': 'v1', "apiVersion": "v1",
'kind': 'Pod', "kind": "Pod",
'metadata': { "metadata": {"name": "test", "labels": {}},
'name': 'test', "spec": {
'labels': {} "containers": [
}, {
'spec': { "name": "nginx",
'containers': [ "image": "nginx:1.14.2",
{'name': 'nginx', 'image': 'nginx:1.14.2', 'command': ['/bin/sh', '-c', 'sleep 10']} "command": ["/bin/sh", "-c", "sleep 10"],
}
] ]
} },
} }
test_definition = { test_definition = {
'apiVersion': 'v1', "apiVersion": "v1",
'kind': 'Pod', "kind": "Pod",
'metadata': { "metadata": {"name": "test", "labels": {"environment": "test", "app": "nginx"}},
'name': 'test', "spec": {
'labels': { "containers": [
'environment': 'test', {
'app': 'nginx', "name": "nginx",
} "image": "nginx:1.15.2",
}, "command": ["/bin/sh", "-c", "sleep 10"],
'spec': { }
'containers': [
{'name': 'nginx', 'image': 'nginx:1.15.2', 'command': ['/bin/sh', '-c', 'sleep 10']}
] ]
} },
} }
@@ -75,13 +78,27 @@ def test_selector_parser():
assert sel._operator == "in" and sel._data == ["true"] and sel._key == "environment" assert sel._operator == "in" and sel._data == ["true"] and sel._key == "environment"
f_selector = "environment!=false" f_selector = "environment!=false"
sel = Selector(f_selector) sel = Selector(f_selector)
assert sel._operator == "notin" and sel._data == ["false"] and sel._key == "environment" assert (
sel._operator == "notin"
and sel._data == ["false"]
and sel._key == "environment"
)
f_selector = "environment notin (true, false)" f_selector = "environment notin (true, false)"
sel = Selector(f_selector) sel = Selector(f_selector)
assert sel._operator == "notin" and "true" in sel._data and "false" in sel._data and sel._key == "environment" assert (
sel._operator == "notin"
and "true" in sel._data
and "false" in sel._data
and sel._key == "environment"
)
f_selector = "environment in (true, false)" f_selector = "environment in (true, false)"
sel = Selector(f_selector) sel = Selector(f_selector)
assert sel._operator == "in" and "true" in sel._data and "false" in sel._data and sel._key == "environment" assert (
sel._operator == "in"
and "true" in sel._data
and "false" in sel._data
and sel._key == "environment"
)
f_selector = "environmentin(true, false)" f_selector = "environmentin(true, false)"
sel = Selector(f_selector) sel = Selector(f_selector)
assert not sel._operator and not sel._data and sel._key == f_selector assert not sel._operator and not sel._data and sel._key == f_selector
@@ -97,91 +114,91 @@ def test_selector_parser():
def test_label_selector_without_operator(): def test_label_selector_without_operator():
label_selector = ['environment', 'app'] label_selector = ["environment", "app"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
def test_label_selector_equal_operator(): def test_label_selector_equal_operator():
label_selector = ['environment==test'] label_selector = ["environment==test"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment=production'] label_selector = ["environment=production"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment=production', 'app==mongodb'] label_selector = ["environment=production", "app==mongodb"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment=production', 'app==nginx'] label_selector = ["environment=production", "app==nginx"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment', 'app==nginx'] label_selector = ["environment", "app==nginx"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
def test_label_selector_notequal_operator(): def test_label_selector_notequal_operator():
label_selector = ['environment!=test'] label_selector = ["environment!=test"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment!=production'] label_selector = ["environment!=production"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment=production', 'app!=mongodb'] label_selector = ["environment=production", "app!=mongodb"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment=production', 'app!=nginx'] label_selector = ["environment=production", "app!=nginx"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment', 'app!=nginx'] label_selector = ["environment", "app!=nginx"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
def test_label_selector_conflicting_definition(): def test_label_selector_conflicting_definition():
label_selector = ['environment==test', 'environment!=test'] label_selector = ["environment==test", "environment!=test"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment==test', 'environment==production'] label_selector = ["environment==test", "environment==production"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
def test_set_based_requirement(): def test_set_based_requirement():
label_selector = ['environment in (production)'] label_selector = ["environment in (production)"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment in (production, test)'] label_selector = ["environment in (production, test)"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment notin (production)'] label_selector = ["environment notin (production)"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment notin (production, test)'] label_selector = ["environment notin (production, test)"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['environment'] label_selector = ["environment"]
assert LabelSelectorFilter(label_selector).isMatching(prod_definition) assert LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert LabelSelectorFilter(label_selector).isMatching(test_definition) assert LabelSelectorFilter(label_selector).isMatching(test_definition)
label_selector = ['!environment'] label_selector = ["!environment"]
assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) assert not LabelSelectorFilter(label_selector).isMatching(prod_definition)
assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) assert LabelSelectorFilter(label_selector).isMatching(no_label_definition)
assert not LabelSelectorFilter(label_selector).isMatching(test_definition) assert not LabelSelectorFilter(label_selector).isMatching(test_definition)

16
tox.ini
View File

@@ -34,11 +34,21 @@ commands=
deps = git+https://github.com/ansible-network/collection_prep deps = git+https://github.com/ansible-network/collection_prep
commands = collection_prep_add_docs -p . commands = collection_prep_add_docs -p .
[testenv:linters] [testenv:black]
deps = yamllint deps =
flake8 black==19.10b0
commands = commands =
black -v --check {toxinidir}/plugins {toxinidir}/tests
[testenv:linters]
deps =
yamllint
flake8
black==19.10b0
commands =
black -v --check {toxinidir}/plugins {toxinidir}/tests
yamllint -s {toxinidir} yamllint -s {toxinidir}
flake8 {toxinidir} flake8 {toxinidir}