refactoring for ansible_module.turbo integration (#313)

* refactoring for ansible_module.turbo integration

This refactoring prepares the integration of `ansible_module.turbo`

- Delay the loading of `common.py`, move the shared structure in
  `args_common`.
- Avoid the use of one single object per module, this to increase the
  amount of Python structure that we can cache.
- Cache the Kubernetes client.

See: https://github.com/ansible-collections/community.kubernetes/pull/270

Co-authored-by: Jill Rouleau <jill.rouleau@bespokess.com>
This commit is contained in:
Gonéri Le Bouder
2021-03-16 17:16:18 -04:00
committed by GitHub
parent 2f6fae322c
commit 39660dd40e
14 changed files with 798 additions and 892 deletions

View File

@@ -117,7 +117,7 @@ connections:
import json import json
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception from ansible_collections.community.kubernetes.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:
@@ -180,7 +180,7 @@ 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 = self.get_api_client(**connection) client = get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host)) name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'): if connection.get('namespaces'):
namespaces = connection['namespaces'] namespaces = connection['namespaces']
@@ -190,7 +190,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
self.get_pods_for_namespace(client, name, namespace) self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace) self.get_services_for_namespace(client, name, namespace)
else: else:
client = self.get_api_client() client = get_api_client()
name = self.get_default_host_name(client.configuration.host) name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client) namespaces = self.get_available_namespaces(client)
for namespace in namespaces: for namespace in namespaces:

View File

@@ -198,7 +198,7 @@ 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.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, get_api_client
try: try:
@@ -235,7 +235,7 @@ class KubernetesLookup(K8sAnsibleMixin):
def run(self, terms, variables=None, **kwargs): def run(self, terms, variables=None, **kwargs):
self.params = kwargs self.params = kwargs
self.client = self.get_api_client() self.client = get_api_client()
cluster_info = kwargs.get('cluster_info') cluster_info = kwargs.get('cluster_info')
if cluster_info == 'version': if cluster_info == 'version':

View File

@@ -0,0 +1,6 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule # noqa: F401

View File

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

View File

@@ -26,6 +26,7 @@ import sys
from datetime import datetime from datetime import datetime
from distutils.version import LooseVersion from distutils.version import LooseVersion
from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_MAP, AUTH_ARG_SPEC)
from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six import iteritems, string_types from ansible.module_utils.six import iteritems, string_types
@@ -99,201 +100,107 @@ except ImportError as e:
K8S_IMP_ERR = traceback.format_exc() K8S_IMP_ERR = traceback.format_exc()
def list_dict_str(value): def configuration_digest(configuration):
if isinstance(value, (list, dict, string_types)): import hashlib
return value m = hashlib.sha256()
raise TypeError for k in AUTH_ARG_MAP:
if not hasattr(configuration, k):
v = None
else:
v = getattr(configuration, k)
if v and k in ["ssl_ca_cert", "cert_file", "key_file"]:
with open(str(v), "r") as fd:
content = fd.read()
m.update(content.encode())
else:
m.update(str(v).encode())
digest = m.hexdigest()
return digest
ARG_ATTRIBUTES_BLACKLIST = ('property_path',) def get_api_client(module=None):
auth = {}
COMMON_ARG_SPEC = { def _raise_or_fail(exc, msg):
'state': { if module:
'default': 'present', module.fail_json(msg % to_native(exc))
'choices': ['present', 'absent'], else:
}, raise exc
'force': {
'type': 'bool',
'default': False,
},
}
RESOURCE_ARG_SPEC = { # If authorization variables aren't defined, look for them in environment variables
'resource_definition': { for true_name, arg_name in AUTH_ARG_MAP.items():
'type': list_dict_str, if module and module.params.get(arg_name):
'aliases': ['definition', 'inline'] auth[true_name] = module.params.get(arg_name)
}, else:
'src': { env_value = os.getenv('K8S_AUTH_{0}'.format(arg_name.upper()), None) or os.getenv('K8S_AUTH_{0}'.format(true_name.upper()), None)
'type': 'path', if env_value is not None:
}, if AUTH_ARG_SPEC[arg_name].get('type') == 'bool':
} env_value = env_value.lower() not in ['0', 'false', 'no']
auth[true_name] = env_value
NAME_ARG_SPEC = { def auth_set(*names):
'kind': {}, return all([auth.get(name) for name in names])
'name': {},
'namespace': {},
'api_version': {
'default': 'v1',
'aliases': ['api', 'version'],
},
}
AUTH_ARG_SPEC = { if auth_set('username', 'password', 'host') or auth_set('api_key', 'host'):
'kubeconfig': { # We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
'type': 'path', pass
}, elif auth_set('kubeconfig') or auth_set('context'):
'context': {}, try:
'host': {}, kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
'api_key': { except Exception as err:
'no_log': True, _raise_or_fail(err, 'Failed to load kubeconfig due to %s')
},
'username': {},
'password': {
'no_log': True,
},
'validate_certs': {
'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',
},
'persist_config': {
'type': 'bool',
},
}
WAIT_ARG_SPEC = dict( else:
wait=dict(type='bool', default=False), # First try to do incluster config, then kubeconfig
wait_sleep=dict(type='int', default=5), try:
wait_timeout=dict(type='int', default=120), kubernetes.config.load_incluster_config()
wait_condition=dict( except kubernetes.config.ConfigException:
type='dict', try:
default=None, kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
options=dict( except Exception as err:
type=dict(), _raise_or_fail(err, 'Failed to load kubeconfig due to %s')
status=dict(type='str', default="True", choices=["True", "False", "Unknown"]),
reason=dict()
)
)
)
DELETE_OPTS_ARG_SPEC = { # Override any values in the default configuration with Ansible parameters
'propagationPolicy': { # As of kubernetes-client v12.0.0, get_default_copy() is required here
'choices': ['Foreground', 'Background', 'Orphan'], try:
}, configuration = kubernetes.client.Configuration().get_default_copy()
'gracePeriodSeconds': { except AttributeError:
'type': 'int', configuration = kubernetes.client.Configuration()
},
'preconditions': { for key, value in iteritems(auth):
'type': 'dict', if key in AUTH_ARG_MAP.keys() and value is not None:
'options': { if key == 'api_key':
'resourceVersion': { setattr(configuration, key, {'authorization': "Bearer {0}".format(value)})
'type': 'str', else:
}, setattr(configuration, key, value)
'uid': {
'type': 'str', digest = configuration_digest(configuration)
} if digest in get_api_client._pool:
} client = get_api_client._pool[digest]
} return client
}
try:
client = DynamicClient(kubernetes.client.ApiClient(configuration))
except Exception as err:
_raise_or_fail(err, 'Failed to get client due to %s')
get_api_client._pool[digest] = client
return client
# Map kubernetes-client parameters to ansible parameters get_api_client._pool = {}
AUTH_ARG_MAP = {
'kubeconfig': 'kubeconfig',
'context': 'context',
'host': 'host',
'api_key': 'api_key',
'username': 'username',
'password': 'password',
'verify_ssl': 'validate_certs',
'ssl_ca_cert': 'ca_cert',
'cert_file': 'client_cert',
'key_file': 'client_key',
'proxy': 'proxy',
'persist_config': 'persist_config',
}
class K8sAnsibleMixin(object): class K8sAnsibleMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, module, *args, **kwargs):
if not HAS_K8S_MODULE_HELPER: if not HAS_K8S_MODULE_HELPER:
self.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR, module.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR,
error=to_native(k8s_import_exception)) error=to_native(k8s_import_exception))
self.openshift_version = openshift.__version__ self.openshift_version = openshift.__version__
if not HAS_YAML: if not HAS_YAML:
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
def get_api_client(self, **auth_params):
auth_params = auth_params or getattr(self, 'params', {})
auth = {}
# If authorization variables aren't defined, look for them in environment variables
for true_name, arg_name in AUTH_ARG_MAP.items():
if auth_params.get(arg_name) is None:
env_value = os.getenv('K8S_AUTH_{0}'.format(arg_name.upper()), None) or os.getenv('K8S_AUTH_{0}'.format(true_name.upper()), None)
if env_value is not None:
if AUTH_ARG_SPEC[arg_name].get('type') == 'bool':
env_value = env_value.lower() not in ['0', 'false', 'no']
auth[true_name] = env_value
else:
auth[true_name] = auth_params[arg_name]
def auth_set(*names):
return all([auth.get(name) for name in names])
if auth_set('username', 'password', 'host') or auth_set('api_key', 'host'):
# We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
pass
elif auth_set('kubeconfig') or auth_set('context'):
try:
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
except Exception as err:
self.fail(msg='Failed to load kubeconfig due to %s' % to_native(err))
else:
# First try to do incluster config, then kubeconfig
try:
kubernetes.config.load_incluster_config()
except kubernetes.config.ConfigException:
try:
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
except Exception as err:
self.fail(msg='Failed to load kubeconfig due to %s' % to_native(err))
# Override any values in the default configuration with Ansible parameters
# As of kubernetes-client v12.0.0, get_default_copy() is required here
try:
configuration = kubernetes.client.Configuration().get_default_copy()
except AttributeError:
configuration = kubernetes.client.Configuration()
for key, value in iteritems(auth):
if key in AUTH_ARG_MAP.keys() and value is not None:
if key == 'api_key':
setattr(configuration, key, {'authorization': "Bearer {0}".format(value)})
else:
setattr(configuration, key, value)
kubernetes.client.Configuration.set_default(configuration)
try:
return DynamicClient(kubernetes.client.ApiClient(configuration))
except Exception as err:
self.fail(msg='Failed to get client due to %s' % to_native(err))
def find_resource(self, kind, api_version, fail=False): def find_resource(self, kind, api_version, fail=False):
for attribute in ['kind', 'name', 'singular_name']: for attribute in ['kind', 'name', 'singular_name']:
@@ -513,8 +420,8 @@ class K8sAnsibleMixin(object):
predicate = _resource_absent predicate = _resource_absent
return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state) return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state)
def set_resource_definitions(self): def set_resource_definitions(self, module):
resource_definition = self.params.get('resource_definition') resource_definition = module.params.get('resource_definition')
self.resource_definitions = [] self.resource_definitions = []
@@ -529,7 +436,7 @@ class K8sAnsibleMixin(object):
else: else:
self.resource_definitions = [resource_definition] self.resource_definitions = [resource_definition]
src = self.params.get('src') src = module.params.get('src')
if src: if src:
self.resource_definitions = self.load_resource_definitions(src) self.resource_definitions = self.load_resource_definitions(src)
try: try:
@@ -539,12 +446,12 @@ class K8sAnsibleMixin(object):
if not resource_definition and not src: if not resource_definition and not src:
implicit_definition = dict( implicit_definition = dict(
kind=self.kind, kind=module.params['kind'],
apiVersion=self.api_version, apiVersion=module.params['api_version'],
metadata=dict(name=self.name) metadata=dict(name=module.params['name'])
) )
if self.namespace: if module.params.get('namespace'):
implicit_definition['metadata']['namespace'] = self.namespace implicit_definition['metadata']['namespace'] = module.params.get('namespace')
self.resource_definitions = [implicit_definition] self.resource_definitions = [implicit_definition]
def check_library_version(self): def check_library_version(self):
@@ -577,7 +484,7 @@ class K8sAnsibleMixin(object):
changed = False changed = False
results = [] results = []
try: try:
self.client = self.get_api_client() self.client = get_api_client()
# Hopefully the kubernetes client will provide its own exception class one day # Hopefully the kubernetes client will provide its own exception class one day
except (urllib3.exceptions.RequestError) as e: except (urllib3.exceptions.RequestError) as e:
self.fail_json(msg="Couldn't connect to Kubernetes: %s" % str(e)) self.fail_json(msg="Couldn't connect to Kubernetes: %s" % str(e))

View File

@@ -1,166 +0,0 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC, K8sAnsibleMixin)
try:
from openshift.dynamic.exceptions import NotFoundError
except ImportError:
pass
SCALE_ARG_SPEC = {
'replicas': {'type': 'int', 'required': True},
'current_replicas': {'type': 'int'},
'resource_version': {},
'wait': {'type': 'bool', 'default': True},
'wait_timeout': {'type': 'int', 'default': 20},
}
class KubernetesAnsibleScaleModule(K8sAnsibleMixin):
def __init__(self, k8s_kind=None, *args, **kwargs):
self.client = None
self.warnings = []
mutually_exclusive = [
('resource_definition', 'src'),
]
module = AnsibleModule(
argument_spec=self.argspec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)
self.module = module
self.params = self.module.params
self.check_mode = self.module.check_mode
self.fail_json = self.module.fail_json
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesAnsibleScaleModule, self).__init__()
self.kind = k8s_kind or self.params.get('kind')
self.api_version = self.params.get('api_version')
self.name = self.params.get('name')
self.namespace = self.params.get('namespace')
self.set_resource_definitions()
def execute_module(self):
definition = self.resource_definitions[0]
self.client = self.get_api_client()
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
api_version = definition['apiVersion']
kind = definition['kind']
current_replicas = self.params.get('current_replicas')
replicas = self.params.get('replicas')
resource_version = self.params.get('resource_version')
wait = self.params.get('wait')
wait_time = self.params.get('wait_timeout')
existing = None
existing_count = None
return_attributes = dict(changed=False, result=dict(), diff=dict())
if wait:
return_attributes['duration'] = 0
resource = self.find_resource(kind, api_version, fail=True)
try:
existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except NotFoundError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
error=exc.value.get('status'))
if self.kind == 'job':
existing_count = existing.spec.parallelism
elif hasattr(existing.spec, 'replicas'):
existing_count = existing.spec.replicas
if existing_count is None:
self.fail_json(msg='Failed to retrieve the available count for the requested object.')
if resource_version and resource_version != existing.metadata.resourceVersion:
self.exit_json(**return_attributes)
if current_replicas is not None and existing_count != current_replicas:
self.exit_json(**return_attributes)
if existing_count != replicas:
return_attributes['changed'] = True
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
return_attributes['result'] = resource.patch(existing.to_dict()).to_dict()
else:
return_attributes = self.scale(resource, existing, replicas, wait, wait_time)
self.exit_json(**return_attributes)
@property
def argspec(self):
args = copy.deepcopy(SCALE_ARG_SPEC)
args.update(RESOURCE_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(AUTH_ARG_SPEC)
return args
def scale(self, resource, existing_object, replicas, wait, wait_time):
name = existing_object.metadata.name
namespace = existing_object.metadata.namespace
kind = existing_object.kind
if not hasattr(resource, 'scale'):
self.fail_json(
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
existing = resource.get(name=name, namespace=namespace)
try:
resource.scale.patch(body=scale_obj)
except Exception as exc:
self.fail_json(msg="Scale request failed: {0}".format(exc))
k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
result = dict()
result['result'] = k8s_obj
result['changed'] = not match
result['diff'] = diffs
if wait:
success, result['result'], result['duration'] = self.wait(resource, scale_obj, 5, wait_time)
if not success:
self.fail_json(msg="Resource scaling timed out", **result)
return result

View File

@@ -263,74 +263,68 @@ result:
import copy import copy
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (
K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC)
WAIT_ARG_SPEC, DELETE_OPTS_ARG_SPEC)
class KubernetesModule(K8sAnsibleMixin): def validate_spec():
return dict(
fail_on_error=dict(type='bool'),
version=dict(),
strict=dict(type='bool', default=True)
)
@property
def validate_spec(self):
return dict(
fail_on_error=dict(type='bool'),
version=dict(),
strict=dict(type='bool', default=True)
)
@property def argspec():
def argspec(self): argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
argument_spec = copy.deepcopy(COMMON_ARG_SPEC) argument_spec.update(copy.deepcopy(NAME_ARG_SPEC))
argument_spec.update(copy.deepcopy(NAME_ARG_SPEC)) 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(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) argument_spec['validate'] = dict(type='dict', default=None, options=validate_spec())
argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) argument_spec['append_hash'] = dict(type='bool', default=False)
argument_spec['append_hash'] = dict(type='bool', default=False) argument_spec['apply'] = dict(type='bool', default=False)
argument_spec['apply'] = dict(type='bool', default=False) argument_spec['template'] = dict(type='raw', default=None)
argument_spec['template'] = dict(type='raw', default=None) argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC))
argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) return argument_spec
return argument_spec
def __init__(self, k8s_kind=None, *args, **kwargs):
mutually_exclusive = [
('resource_definition', 'src'),
('merge_type', 'apply'),
('template', 'resource_definition'),
('template', 'src'),
]
module = AnsibleModule( def execute_module(module, k8s_ansible_mixin):
argument_spec=self.argspec, k8s_ansible_mixin.module = module
mutually_exclusive=mutually_exclusive, k8s_ansible_mixin.argspec = module.argument_spec
supports_check_mode=True, k8s_ansible_mixin.check_mode = k8s_ansible_mixin.module.check_mode
) k8s_ansible_mixin.params = k8s_ansible_mixin.module.params
k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json
k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json
k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json
k8s_ansible_mixin.warnings = []
self.module = module k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get('kind')
self.check_mode = self.module.check_mode k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get('api_version')
self.params = self.module.params k8s_ansible_mixin.name = k8s_ansible_mixin.params.get('name')
self.fail_json = self.module.fail_json k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get('namespace')
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesModule, self).__init__(*args, **kwargs) k8s_ansible_mixin.check_library_version()
k8s_ansible_mixin.set_resource_definitions(module)
self.client = None k8s_ansible_mixin.execute_module()
self.warnings = []
self.kind = k8s_kind or self.params.get('kind')
self.api_version = self.params.get('api_version')
self.name = self.params.get('name')
self.namespace = self.params.get('namespace')
self.check_library_version()
self.set_resource_definitions()
def main(): def main():
KubernetesModule().execute_module() mutually_exclusive = [
('resource_definition', 'src'),
('merge_type', 'apply'),
('template', 'resource_definition'),
('template', 'src'),
]
module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -159,88 +159,60 @@ apis:
import copy import copy
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, AUTH_ARG_SPEC from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC)
try:
try:
from openshift import __version__ as version
# >=0.10
from openshift.dynamic.resource import ResourceList
except ImportError:
# <0.10
from openshift.dynamic.client import ResourceList
HAS_K8S_INSTANCE_HELPER = True
k8s_import_exception = None
except ImportError:
HAS_K8S_INSTANCE_HELPER = False
k8s_import_exception = traceback.format_exc()
class KubernetesInfoModule(K8sAnsibleMixin): def execute_module(module, client):
invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False)
def __init__(self): if invalidate_cache:
module = AnsibleModule( client.resources.invalidate_cache()
argument_spec=self.argspec, results = {}
supports_check_mode=True, from openshift.dynamic.resource import ResourceList
) for resource in list(client.resources):
self.module = module resource = resource[0]
self.params = self.module.params if isinstance(resource, ResourceList):
continue
if not HAS_K8S_INSTANCE_HELPER: results[resource.group] = {
self.module.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type"), 'api_version': resource.group_version,
exception=k8s_import_exception) 'categories': resource.categories if resource.categories else [],
'kind': resource.kind,
super(KubernetesInfoModule, self).__init__() 'name': resource.name,
'namespaced': resource.namespaced,
def execute_module(self): 'preferred': resource.preferred,
self.client = self.get_api_client() 'short_names': resource.short_names if resource.short_names else [],
invalidate_cache = boolean(self.module.params.get('invalidate_cache', True), strict=False) 'singular_name': resource.singular_name,
if invalidate_cache:
self.client.resources.invalidate_cache()
results = {}
for resource in list(self.client.resources):
resource = resource[0]
if isinstance(resource, ResourceList):
continue
results[resource.group] = {
'api_version': resource.group_version,
'categories': resource.categories if resource.categories else [],
'kind': resource.kind,
'name': resource.name,
'namespaced': resource.namespaced,
'preferred': resource.preferred,
'short_names': resource.short_names if resource.short_names else [],
'singular_name': resource.singular_name,
}
configuration = self.client.configuration
connection = {
'cert_file': configuration.cert_file,
'host': configuration.host,
'password': configuration.password,
'proxy': configuration.proxy,
'ssl_ca_cert': configuration.ssl_ca_cert,
'username': configuration.username,
'verify_ssl': configuration.verify_ssl,
} }
version_info = { configuration = client.configuration
'client': version, connection = {
'server': self.client.version, 'cert_file': configuration.cert_file,
} 'host': configuration.host,
self.module.exit_json(changed=False, apis=results, connection=connection, version=version_info) 'password': configuration.password,
'proxy': configuration.proxy,
'ssl_ca_cert': configuration.ssl_ca_cert,
'username': configuration.username,
'verify_ssl': configuration.verify_ssl,
}
from openshift import __version__ as version
version_info = {
'client': version,
'server': client.version,
}
module.exit_json(changed=False, apis=results, connection=connection, version=version_info)
@property
def argspec(self): 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():
KubernetesInfoModule().execute_module() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import get_api_client
execute_module(module, client=get_api_client(module=module))
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -118,10 +118,10 @@ except ImportError:
# ImportError are managed by the common module already. # ImportError are managed by the common module already.
pass pass
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.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.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, AUTH_ARG_SPEC AUTH_ARG_SPEC
) )
try: try:
@@ -132,75 +132,72 @@ except ImportError:
pass pass
class KubernetesExecCommand(K8sAnsibleMixin): def argspec():
spec = copy.deepcopy(AUTH_ARG_SPEC)
spec['namespace'] = dict(type='str', required=True)
spec['pod'] = dict(type='str', required=True)
spec['container'] = dict(type='str')
spec['command'] = dict(type='str', required=True)
return spec
def __init__(self):
module = AnsibleModule(
argument_spec=self.argspec,
supports_check_mode=True,
)
self.module = module
self.params = self.module.params
self.fail_json = self.module.fail_json
super(KubernetesExecCommand, self).__init__()
@property def execute_module(module, k8s_ansible_mixin):
def argspec(self):
spec = copy.deepcopy(AUTH_ARG_SPEC)
spec['namespace'] = dict(type='str', required=True)
spec['pod'] = dict(type='str', required=True)
spec['container'] = dict(type='str')
spec['command'] = dict(type='str', required=True)
return spec
def execute_module(self): # Load kubernetes.client.Configuration
# Load kubernetes.client.Configuration api = core_v1_api.CoreV1Api()
self.get_api_client()
api = core_v1_api.CoreV1Api()
# hack because passing the container as None breaks things # hack because passing the container as None breaks things
optional_kwargs = {} optional_kwargs = {}
if self.params.get('container'): if module.params.get('container'):
optional_kwargs['container'] = self.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,
self.params["pod"], module.params["pod"],
self.params["namespace"], module.params["namespace"],
command=shlex.split(self.params["command"]), command=shlex.split(module.params["command"]),
stdout=True, stdout=True,
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:
self.module.fail_json(msg="Failed to execute on pod %s" module.fail_json(msg="Failed to execute on pod %s"
" due to : %s" % (self.params.get('pod'), to_native(e))) " 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)
if resp.peek_stdout(): if resp.peek_stdout():
stdout.append(resp.read_stdout()) stdout.append(resp.read_stdout())
if resp.peek_stderr(): if resp.peek_stderr():
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'])
self.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),
return_code=rc return_code=rc
) )
def main(): def main():
KubernetesExecCommand().execute_module() module = AnsibleModule(
argument_spec=argspec(),
supports_check_mode=True,
)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -148,58 +148,50 @@ resources:
import copy import copy
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC, WAIT_ARG_SPEC)
K8sAnsibleMixin, AUTH_ARG_SPEC, WAIT_ARG_SPEC)
class KubernetesInfoModule(K8sAnsibleMixin): def execute_module(module, k8s_ansible_mixin):
facts = k8s_ansible_mixin.kubernetes_facts(
module.params["kind"],
module.params["api_version"],
name=module.params["name"],
namespace=module.params["namespace"],
label_selectors=module.params["label_selectors"],
field_selectors=module.params["field_selectors"],
wait=module.params["wait"],
wait_sleep=module.params["wait_sleep"],
wait_timeout=module.params["wait_timeout"],
condition=module.params["wait_condition"],
)
module.exit_json(changed=False, **facts)
def __init__(self, *args, **kwargs):
module = AnsibleModule( def argspec():
argument_spec=self.argspec, args = copy.deepcopy(AUTH_ARG_SPEC)
supports_check_mode=True, args.update(WAIT_ARG_SPEC)
args.update(
dict(
kind=dict(required=True),
api_version=dict(default='v1', aliases=['api', 'version']),
name=dict(),
namespace=dict(),
label_selectors=dict(type='list', elements='str', default=[]),
field_selectors=dict(type='list', elements='str', default=[]),
) )
self.module = module )
self.params = self.module.params return args
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesInfoModule, self).__init__()
def execute_module(self):
self.client = self.get_api_client()
self.exit_json(changed=False,
**self.kubernetes_facts(self.params['kind'],
self.params['api_version'],
name=self.params['name'],
namespace=self.params['namespace'],
label_selectors=self.params['label_selectors'],
field_selectors=self.params['field_selectors'],
wait=self.params['wait'],
wait_sleep=self.params['wait_sleep'],
wait_timeout=self.params['wait_timeout'],
condition=self.params['wait_condition']))
@property
def argspec(self):
args = copy.deepcopy(AUTH_ARG_SPEC)
args.update(WAIT_ARG_SPEC)
args.update(
dict(
kind=dict(required=True),
api_version=dict(default='v1', aliases=['api', 'version']),
name=dict(),
namespace=dict(),
label_selectors=dict(type='list', elements='str', default=[]),
field_selectors=dict(type='list', elements='str', default=[]),
)
)
return args
def main(): def main():
KubernetesInfoModule().execute_module() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -111,116 +111,101 @@ log_lines:
import copy import copy
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible.module_utils.six import PY2 from ansible.module_utils.six import PY2
from ansible_collections.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC, NAME_ARG_SPEC)
K8sAnsibleMixin, AUTH_ARG_SPEC, NAME_ARG_SPEC)
class KubernetesLogModule(K8sAnsibleMixin): def argspec():
args = copy.deepcopy(AUTH_ARG_SPEC)
def __init__(self): args.update(NAME_ARG_SPEC)
module = AnsibleModule( args.update(
argument_spec=self.argspec, dict(
supports_check_mode=True, kind=dict(type='str', default='Pod'),
container=dict(),
label_selectors=dict(type='list', elements='str', default=[]),
) )
self.module = module )
self.params = self.module.params return args
self.fail_json = self.module.fail_json
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesLogModule, self).__init__()
@property
def argspec(self):
args = copy.deepcopy(AUTH_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(
dict(
kind=dict(type='str', default='Pod'),
container=dict(),
label_selectors=dict(type='list', elements='str', default=[]),
)
)
return args
def execute_module(self): def execute_module(module, k8s_ansible_mixin):
name = self.params.get('name') name = module.params.get('name')
namespace = self.params.get('namespace') namespace = module.params.get('namespace')
label_selector = ','.join(self.params.get('label_selectors', {})) label_selector = ','.join(module.params.get('label_selectors', {}))
if name and label_selector: if name and label_selector:
self.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')
self.client = self.get_api_client() resource = k8s_ansible_mixin.find_resource(module.params['kind'], module.params['api_version'], fail=True)
resource = self.find_resource(self.params['kind'], self.params['api_version'], fail=True) v1_pods = k8s_ansible_mixin.find_resource('Pod', 'v1', fail=True)
v1_pods = self.find_resource('Pod', 'v1', fail=True)
if 'log' not in resource.subresources: if 'log' not in resource.subresources:
if not name: if not name:
self.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(self.extract_selectors(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:
self.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 self.params.get('container'): if module.params.get('container'):
kwargs['query_params'] = dict(container=self.params['container']) kwargs['query_params'] = dict(container=module.params['container'])
log = serialize_log(resource.log.get( log = serialize_log(resource.log.get(
name=name, name=name,
namespace=namespace, namespace=namespace,
serialize=False, serialize=False,
**kwargs **kwargs
)) ))
self.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(self, instance):
# Parses selectors on an object based on the specifications documented here:
# https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
selectors = []
if not instance.spec.selector:
self.fail(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): def extract_selectors(module, instance):
# A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions # Parses selectors on an object based on the specifications documented here:
for k, v in dict(instance.spec.selector).items(): # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
selectors.append('{0}={1}'.format(k, v)) selectors = []
return selectors 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(
if instance.spec.selector.matchLabels: '/'.join(instance.group, instance.apiVersion), instance.kind))
for k, v in dict(instance.spec.selector.matchLabels).items():
selectors.append('{0}={1}'.format(k, v))
if instance.spec.selector.matchExpressions:
for expression in instance.spec.selector.matchExpressions:
operator = expression.operator
if operator == 'Exists':
selectors.append(expression.key)
elif operator == 'DoesNotExist':
selectors.append('!{0}'.format(expression.key))
elif operator in ['In', 'NotIn']:
selectors.append('{key} {operator} {values}'.format(
key=expression.key,
operator=operator.lower(),
values='({0})'.format(', '.join(expression.values))
))
else:
self.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower()))
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
for k, v in dict(instance.spec.selector).items():
selectors.append('{0}={1}'.format(k, v))
return selectors return selectors
if instance.spec.selector.matchLabels:
for k, v in dict(instance.spec.selector.matchLabels).items():
selectors.append('{0}={1}'.format(k, v))
if instance.spec.selector.matchExpressions:
for expression in instance.spec.selector.matchExpressions:
operator = expression.operator
if operator == 'Exists':
selectors.append(expression.key)
elif operator == 'DoesNotExist':
selectors.append('!{0}'.format(expression.key))
elif operator in ['In', 'NotIn']:
selectors.append('{key} {operator} {values}'.format(
key=expression.key,
operator=operator.lower(),
values='({0})'.format(', '.join(expression.values))
))
else:
module.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower()))
return selectors
def serialize_log(response): def serialize_log(response):
if PY2: if PY2:
@@ -229,7 +214,13 @@ def serialize_log(response):
def main(): def main():
KubernetesLogModule().execute_module() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -78,127 +78,117 @@ rollback_info:
import copy import copy
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (
K8sAnsibleMixin, AUTH_ARG_SPEC, NAME_ARG_SPEC) AUTH_ARG_SPEC, NAME_ARG_SPEC)
class KubernetesRollbackModule(K8sAnsibleMixin): def get_managed_resource(module):
managed_resource = {}
def __init__(self): kind = module.params['kind']
module = AnsibleModule( if kind == "DaemonSet":
argument_spec=self.argspec, managed_resource['kind'] = "ControllerRevision"
supports_check_mode=True, managed_resource['api_version'] = "apps/v1"
elif kind == "Deployment":
managed_resource['kind'] = "ReplicaSet"
managed_resource['api_version'] = "apps/v1"
else:
module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind))
return managed_resource
def execute_module(module, k8s_ansible_mixin):
results = []
resources = k8s_ansible_mixin.kubernetes_facts(
module.params['kind'],
module.params['api_version'],
module.params['name'],
module.params['namespace'],
module.params['label_selectors'],
module.params['field_selectors'])
for resource in resources['resources']:
result = perform_action(module, k8s_ansible_mixin, resource)
results.append(result)
module.exit_json(**{
'changed': True,
'rollback_info': results
})
def perform_action(module, k8s_ansible_mixin, resource):
if module.params['kind'] == "DaemonSet":
current_revision = resource['metadata']['generation']
elif module.params['kind'] == "Deployment":
current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision']
managed_resource = get_managed_resource(module)
managed_resources = k8s_ansible_mixin.kubernetes_facts(
managed_resource['kind'],
managed_resource['api_version'],
'',
module.params['namespace'],
resource['spec']
['selector']
['matchLabels'],
'')
prev_managed_resource = get_previous_revision(managed_resources['resources'],
current_revision)
if module.params['kind'] == "Deployment":
del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash']
resource_patch = [{
"op": "replace",
"path": "/spec/template",
"value": prev_managed_resource['spec']['template']
}, {
"op": "replace",
"path": "/metadata/annotations",
"value": {
"deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision']
}
}]
api_target = 'deployments'
content_type = 'application/json-patch+json'
elif module.params['kind'] == "DaemonSet":
resource_patch = prev_managed_resource["data"]
api_target = 'daemonsets'
content_type = 'application/strategic-merge-patch+json'
rollback = k8s_ansible_mixin.client.request(
"PATCH",
"/apis/{0}/namespaces/{1}/{2}/{3}"
.format(module.params['api_version'],
module.params['namespace'],
api_target,
module.params['name']),
body=resource_patch,
content_type=content_type)
result = {'changed': True}
result['method'] = 'patch'
result['body'] = resource_patch
result['resources'] = rollback.to_dict()
return result
def argspec():
args = copy.deepcopy(AUTH_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(
dict(
label_selectors=dict(type='list', elements='str', default=[]),
field_selectors=dict(type='list', elements='str', default=[]),
) )
self.module = module )
self.params = self.module.params return args
self.fail_json = self.module.fail_json
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesRollbackModule, self).__init__()
self.kind = self.params['kind']
self.api_version = self.params['api_version']
self.name = self.params['name']
self.namespace = self.params['namespace']
self.managed_resource = {}
if self.kind == "DaemonSet":
self.managed_resource['kind'] = "ControllerRevision"
self.managed_resource['api_version'] = "apps/v1"
elif self.kind == "Deployment":
self.managed_resource['kind'] = "ReplicaSet"
self.managed_resource['api_version'] = "apps/v1"
else:
self.fail(msg="Cannot perform rollback on resource of kind {0}".format(self.kind))
def execute_module(self):
results = []
self.client = self.get_api_client()
resources = self.kubernetes_facts(self.kind,
self.api_version,
self.name,
self.namespace,
self.params['label_selectors'],
self.params['field_selectors'])
for resource in resources['resources']:
result = self.perform_action(resource)
results.append(result)
self.exit_json(**{
'changed': True,
'rollback_info': results
})
def perform_action(self, resource):
if self.kind == "DaemonSet":
current_revision = resource['metadata']['generation']
elif self.kind == "Deployment":
current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision']
managed_resources = self.kubernetes_facts(self.managed_resource['kind'],
self.managed_resource['api_version'],
'',
self.namespace,
resource['spec']
['selector']
['matchLabels'],
'')
prev_managed_resource = get_previous_revision(managed_resources['resources'],
current_revision)
if self.kind == "Deployment":
del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash']
resource_patch = [{
"op": "replace",
"path": "/spec/template",
"value": prev_managed_resource['spec']['template']
}, {
"op": "replace",
"path": "/metadata/annotations",
"value": {
"deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision']
}
}]
api_target = 'deployments'
content_type = 'application/json-patch+json'
elif self.kind == "DaemonSet":
resource_patch = prev_managed_resource["data"]
api_target = 'daemonsets'
content_type = 'application/strategic-merge-patch+json'
rollback = self.client.request("PATCH",
"/apis/{0}/namespaces/{1}/{2}/{3}"
.format(self.api_version,
self.namespace,
api_target,
self.name),
body=resource_patch,
content_type=content_type)
result = {'changed': True}
result['method'] = 'patch'
result['body'] = resource_patch
result['resources'] = rollback.to_dict()
return result
@property
def argspec(self):
args = copy.deepcopy(AUTH_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(
dict(
label_selectors=dict(type='list', elements='str', default=[]),
field_selectors=dict(type='list', elements='str', default=[]),
)
)
return args
def get_previous_revision(all_resources, current_revision): def get_previous_revision(all_resources, current_revision):
@@ -217,7 +207,12 @@ def get_previous_revision(all_resources, current_revision):
def main(): def main():
KubernetesRollbackModule().execute_module() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -118,11 +118,129 @@ result:
sample: 48 sample: 48
''' '''
from ansible_collections.community.kubernetes.plugins.module_utils.scale import KubernetesAnsibleScaleModule import copy
from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC)
SCALE_ARG_SPEC = {
'replicas': {'type': 'int', 'required': True},
'current_replicas': {'type': 'int'},
'resource_version': {},
'wait': {'type': 'bool', 'default': True},
'wait_timeout': {'type': 'int', 'default': 20},
}
def execute_module(module, k8s_ansible_mixin,):
k8s_ansible_mixin.set_resource_definitions(module)
definition = k8s_ansible_mixin.resource_definitions[0]
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
api_version = definition['apiVersion']
kind = definition['kind']
current_replicas = module.params.get('current_replicas')
replicas = module.params.get('replicas')
resource_version = module.params.get('resource_version')
wait = module.params.get('wait')
wait_time = module.params.get('wait_timeout')
existing = None
existing_count = None
return_attributes = dict(changed=False, result=dict(), diff=dict())
if wait:
return_attributes['duration'] = 0
resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import NotFoundError
try:
existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except NotFoundError as exc:
module.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
error=exc.value.get('status'))
if module.params['kind'] == 'job':
existing_count = existing.spec.parallelism
elif hasattr(existing.spec, 'replicas'):
existing_count = existing.spec.replicas
if existing_count is None:
module.fail_json(msg='Failed to retrieve the available count for the requested object.')
if resource_version and resource_version != existing.metadata.resourceVersion:
module.exit_json(**return_attributes)
if current_replicas is not None and existing_count != current_replicas:
module.exit_json(**return_attributes)
if existing_count != replicas:
return_attributes['changed'] = True
if not module.check_mode:
if module.params['kind'] == 'job':
existing.spec.parallelism = replicas
return_attributes['result'] = resource.patch(existing.to_dict()).to_dict()
else:
return_attributes = scale(module, k8s_ansible_mixin, resource, existing, replicas, wait, wait_time)
module.exit_json(**return_attributes)
def argspec():
args = copy.deepcopy(SCALE_ARG_SPEC)
args.update(RESOURCE_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(AUTH_ARG_SPEC)
return args
def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait, wait_time):
name = existing_object.metadata.name
namespace = existing_object.metadata.namespace
kind = existing_object.kind
if not hasattr(resource, 'scale'):
module.fail_json(
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
existing = resource.get(name=name, namespace=namespace)
try:
resource.scale.patch(body=scale_obj)
except Exception as exc:
module.fail_json(msg="Scale request failed: {0}".format(exc))
k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
result = dict()
result['result'] = k8s_obj
result['changed'] = not match
result['diff'] = diffs
if wait:
success, result['result'], result['duration'] = k8s_ansible_mixin.wait(resource, scale_obj, 5, wait_time)
if not success:
module.fail_json(msg="Resource scaling timed out", **result)
return result
def main(): def main():
KubernetesAnsibleScaleModule().execute_module() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -145,14 +145,12 @@ result:
''' '''
import copy import copy
import traceback
from collections import defaultdict from collections import defaultdict
from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import ( from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (
K8sAnsibleMixin, 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': {
@@ -173,100 +171,69 @@ SERVICE_ARG_SPEC = {
} }
class KubernetesService(K8sAnsibleMixin): def merge_dicts(x, y):
def __init__(self, *args, **kwargs): for k in set(x.keys()).union(y.keys()):
mutually_exclusive = [ if k in x and k in y:
('resource_definition', 'src'), if isinstance(x[k], dict) and isinstance(y[k], dict):
('merge_type', 'apply'), yield (k, dict(merge_dicts(x[k], y[k])))
]
module = AnsibleModule(
argument_spec=self.argspec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)
self.module = module
self.check_mode = self.module.check_mode
self.params = self.module.params
self.fail_json = self.module.fail_json
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(KubernetesService, self).__init__(*args, **kwargs)
self.client = None
self.warnings = []
self.kind = self.params.get('kind')
self.api_version = self.params.get('api_version')
self.name = self.params.get('name')
self.namespace = self.params.get('namespace')
self.check_library_version()
self.set_resource_definitions()
@staticmethod
def merge_dicts(x, y):
for k in set(x.keys()).union(y.keys()):
if k in x and k in y:
if isinstance(x[k], dict) and isinstance(y[k], dict):
yield (k, dict(KubernetesService.merge_dicts(x[k], y[k])))
else:
yield (k, y[k])
elif k in x:
yield (k, x[k])
else: else:
yield (k, y[k]) yield (k, y[k])
elif k in x:
yield (k, x[k])
else:
yield (k, y[k])
@property
def argspec(self):
""" argspec property builder """
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec.update(COMMON_ARG_SPEC)
argument_spec.update(RESOURCE_ARG_SPEC)
argument_spec.update(SERVICE_ARG_SPEC)
return argument_spec
def execute_module(self): def argspec():
""" Module execution """ """ argspec property builder """
self.client = self.get_api_client() argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec.update(COMMON_ARG_SPEC)
argument_spec.update(RESOURCE_ARG_SPEC)
argument_spec.update(SERVICE_ARG_SPEC)
return argument_spec
api_version = 'v1'
selector = self.params.get('selector')
service_type = self.params.get('type')
ports = self.params.get('ports')
definition = defaultdict(defaultdict) def execute_module(module, k8s_ansible_mixin):
""" Module execution """
k8s_ansible_mixin.set_resource_definitions(module)
definition['kind'] = 'Service' api_version = 'v1'
definition['apiVersion'] = api_version selector = module.params.get('selector')
service_type = module.params.get('type')
ports = module.params.get('ports')
def_spec = definition['spec'] definition = defaultdict(defaultdict)
def_spec['type'] = service_type
def_spec['ports'] = ports
def_spec['selector'] = selector
def_meta = definition['metadata'] definition['kind'] = 'Service'
def_meta['name'] = self.params.get('name') definition['apiVersion'] = api_version
def_meta['namespace'] = self.params.get('namespace')
# 'resource_definition:' has lower priority than module parameters def_spec = definition['spec']
definition = dict(self.merge_dicts(self.resource_definitions[0], definition)) def_spec['type'] = service_type
def_spec['ports'] = ports
def_spec['selector'] = selector
resource = self.find_resource('Service', api_version, fail=True) def_meta = definition['metadata']
definition = self.set_defaults(resource, definition) def_meta['name'] = module.params.get('name')
result = self.perform_action(resource, definition) def_meta['namespace'] = module.params.get('namespace')
self.exit_json(**result) # 'resource_definition:' has lower priority than module parameters
definition = dict(merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition))
resource = k8s_ansible_mixin.find_resource('Service', api_version, fail=True)
definition = k8s_ansible_mixin.set_defaults(resource, definition)
result = k8s_ansible_mixin.perform_action(resource, definition)
module.exit_json(**result)
def main(): def main():
module = KubernetesService() module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
try: from ansible_collections.community.kubernetes.plugins.module_utils.common import (
module.execute_module() K8sAnsibleMixin, get_api_client)
except Exception as e:
module.fail_json(msg=str(e), exception=traceback.format_exc()) k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__': if __name__ == '__main__':