Merge remote-tracking branch 'upstream/main' into merge-upstream

This commit is contained in:
Shaun Smiley
2020-09-18 13:22:53 -07:00
123 changed files with 3666 additions and 988 deletions

1
plugins/action/helm.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

1
plugins/action/helm_info.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -0,0 +1 @@
k8s_info.py

1
plugins/action/k8s.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

1
plugins/action/k8s_auth.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

1
plugins/action/k8s_exec.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -0,0 +1,82 @@
# Copyright (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# Copyright (c) 2017, Toshio Kuratomi <tkuraotmi@ansible.com>
# Copyright (c) 2020, Ansible Project
# 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)
__metaclass__ = type
import copy
import traceback
from ansible.module_utils._text import to_text
from ansible.plugins.action import ActionBase
from ansible.errors import AnsibleError
class ActionModule(ActionBase):
TRANSFERS_FILES = True
def _ensure_invocation(self, result):
# NOTE: adding invocation arguments here needs to be kept in sync with
# any no_log specified in the argument_spec in the module.
if 'invocation' not in result:
if self._play_context.no_log:
result['invocation'] = "CENSORED: no_log is set"
else:
result['invocation'] = self._task.args.copy()
result['invocation']['module_args'] = self._task.args.copy()
return result
def run(self, tmp=None, task_vars=None):
''' handler for k8s options '''
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
new_module_args = copy.deepcopy(self._task.args)
kubeconfig = self._task.args.get('kubeconfig', None)
# find the file in the expected search path
if kubeconfig:
try:
# find in expected paths
kubeconfig = self._find_needle('files', kubeconfig)
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_text(e)
result['exception'] = traceback.format_exc()
return result
if kubeconfig:
# decrypt kubeconfig found
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
new_module_args['kubeconfig'] = actual_file
# find the file in the expected search path
src = self._task.args.get('src', None)
if src:
try:
# find in expected paths
src = self._find_needle('files', src)
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_text(e)
result['exception'] = traceback.format_exc()
return result
if src:
new_module_args['src'] = src
# Execute the k8s_* module.
module_return = self._execute_module(module_name=self._task.action, module_args=new_module_args, task_vars=task_vars)
# Delete tmp path
self._remove_tmp_path(self._connection._shell.tmpdir)
result.update(module_return)
return self._ensure_invocation(result)

1
plugins/action/k8s_log.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

1
plugins/action/k8s_scale.py Symbolic link
View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -0,0 +1 @@
k8s_info.py

View File

@@ -20,7 +20,7 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
DOCUMENTATION = r"""
author:
- xuxinkun
@@ -38,7 +38,8 @@ DOCUMENTATION = """
options:
kubectl_pod:
description:
- Pod name. Required when the host name does not match pod name.
- Pod name.
- Required when the host name does not match pod name.
default: ''
vars:
- name: ansible_kubectl_pod
@@ -46,7 +47,8 @@ DOCUMENTATION = """
- name: K8S_AUTH_POD
kubectl_container:
description:
- Container name. Required when a pod contains more than one container.
- Container name.
- Required when a pod contains more than one container.
default: ''
vars:
- name: ansible_kubectl_container
@@ -173,7 +175,6 @@ import os
import os.path
import subprocess
import ansible.constants as C
from ansible.parsing.yaml.loader import AnsibleLoader
from ansible.errors import AnsibleError, AnsibleFileNotFound
from ansible.module_utils.six.moves import shlex_quote
@@ -228,6 +229,7 @@ class Connection(ConnectionBase):
""" Build the local kubectl exec command to run cmd on remote_host
"""
local_cmd = [self.transport_cmd]
censored_local_cmd = [self.transport_cmd]
# Build command options based on doc string
doc_yaml = AnsibleLoader(self.documentation).get_single_data()
@@ -236,28 +238,36 @@ class Connection(ConnectionBase):
# Translate verify_ssl to skip_verify_ssl, and output as string
skip_verify_ssl = not self.get_option(key)
local_cmd.append(u'{0}={1}'.format(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]
local_cmd += [cmd_arg, self.get_option(key)]
# Redact password and token from console log
if key.endswith(('_token', '_password')):
censored_local_cmd += [cmd_arg, '********']
extra_args_name = u'{0}_extra_args'.format(self.transport)
if self.get_option(extra_args_name):
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))
if not pod:
pod = self._play_context.remote_addr
# -i is needed to keep stdin open which allows pipelining to work
local_cmd += ['exec', '-i', pod]
censored_local_cmd += ['exec', '-i', pod]
# if the pod has more than one container, then container is required
container_arg_name = u'{0}_container'.format(self.transport)
if 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)]
local_cmd += ['--'] + cmd
censored_local_cmd += ['--'] + cmd
return local_cmd
return local_cmd, censored_local_cmd
def _connect(self, port=None):
""" Connect to the container. Nothing to do """
@@ -270,9 +280,9 @@ class Connection(ConnectionBase):
""" Run a command in the container """
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
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" % (local_cmd,), host=self._play_context.remote_addr)
display.vvv("EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr)
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)
@@ -312,7 +322,7 @@ class Connection(ConnectionBase):
count = ' count=0'
else:
count = ''
args = 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([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:
p = subprocess.Popen(args, stdin=in_file,
@@ -334,7 +344,7 @@ class Connection(ConnectionBase):
# kubectl doesn't have native support for fetching files from
# running containers, so we use kubectl exec to implement this
args = self._build_exec_cmd([self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)])
args, dummy = self._build_exec_cmd([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))
with open(to_bytes(actual_out_path, errors='surrogate_or_strict'), 'wb') as out_file:

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# Copyright: (c) 2020, Red Hat Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Options for common Helm modules
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
binary_path:
description:
- The path of a helm binary to use.
required: false
type: path
context:
description:
- Helm option to specify which kubeconfig context to use.
- If the value is not specified in the task, the value of environment variable C(K8S_AUTH_CONTEXT) will be used instead.
type: str
aliases: [ kube_context ]
kubeconfig:
description:
- Helm option to specify kubeconfig path to use.
- If the value is not specified in the task, the value of environment variable C(K8S_AUTH_KUBECONFIG) will be used instead.
type: path
aliases: [ kubeconfig_path ]
'''

View File

@@ -15,9 +15,10 @@ class ModuleDocFragment(object):
options:
api_version:
description:
- Use to specify the API version. Use to create, delete, or discover an object without providing a full
resource definition. Use in conjunction with I(kind), I(name), and I(namespace) to identify a
specific object. If I(resource definition) is provided, the I(apiVersion) from the I(resource_definition)
- Use to specify the API version.
- Use to create, delete, or discover an object without providing a full resource definition.
- Use in conjunction with I(kind), I(name), and I(namespace) to identify a specific object.
- If I(resource definition) is provided, the I(apiVersion) value from the I(resource_definition)
will override this option.
type: str
default: v1
@@ -26,23 +27,26 @@ options:
- version
kind:
description:
- Use to specify an object model. Use to create, delete, or discover an object without providing a full
resource definition. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a
specific object. If I(resource definition) is provided, the I(kind) from the I(resource_definition)
- Use to specify an object model.
- Use to create, delete, or discover an object without providing a full resource definition.
- Use in conjunction with I(api_version), I(name), and I(namespace) to identify a specific object.
- If I(resource definition) is provided, the I(kind) value from the I(resource_definition)
will override this option.
type: str
name:
description:
- Use to specify an object name. Use to create, delete, or discover an object without providing a full
resource definition. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a
specific object. If I(resource definition) is provided, the I(metadata.name) value from the
I(resource_definition) will override this option.
- Use to specify an object name.
- Use to create, delete, or discover an object without providing a full resource definition.
- Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a specific object.
- If I(resource definition) is provided, the I(metadata.name) value from the I(resource_definition)
will override this option.
type: str
namespace:
description:
- Use to specify an object namespace. Useful when creating, deleting, or discovering an object without
providing a full resource definition. Use in conjunction with I(api_version), I(kind), and I(name)
to identify a specfic object. If I(resource definition) is provided, the I(metadata.namespace) value
from the I(resource_definition) will override this option.
- Use to specify an object namespace.
- Useful when creating, deleting, or discovering an object without providing a full resource definition.
- Use in conjunction with I(api_version), I(kind), and I(name) to identify a specific object.
- If I(resource definition) is provided, the I(metadata.namespace) value from the I(resource_definition)
will override this option.
type: str
'''

View File

@@ -15,8 +15,8 @@ class ModuleDocFragment(object):
options:
resource_definition:
description:
- "Provide a valid YAML definition (either as a string, list, or dict) for an object when creating or updating. NOTE: I(kind), I(api_version), I(name),
and I(namespace) will be overwritten by corresponding values found in the provided I(resource_definition)."
- Provide a valid YAML definition (either as a string, list, or dict) for an object when creating or updating.
- "NOTE: I(kind), I(api_version), I(name), and I(namespace) will be overwritten by corresponding values found in the provided I(resource_definition)."
aliases:
- definition
- inline

View File

@@ -6,13 +6,6 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
try:
from openshift.helper.hashes import generate_hash
HAS_GENERATE_HASH = True

View File

@@ -28,60 +28,61 @@ DOCUMENTATION = '''
- Optional list of cluster connection settings. If no connections are provided, the default
I(~/.kube/config) and active context will be used, and objects will be returned for all namespaces
the active user is authorized to access.
name:
description:
- Optional name to assign to the cluster. If not provided, a name is constructed from the server
and port.
kubeconfig:
description:
- Path to an existing Kubernetes config file. If not provided, and no other connection
options are provided, the OpenShift client will attempt to load the default
configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG
environment variable.
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment
variable.
host:
description:
- Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.
api_key:
description:
- Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment
variable.
username:
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME
environment variable.
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD
environment variable.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment variable.
aliases: [ cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE
environment variable.
aliases: [ key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API. Can also be specified via
K8S_AUTH_SSL_CA_CERT environment variable.
aliases: [ ssl_ca_cert ]
validate_certs:
description:
- "Whether or not to verify the API server's SSL certificates. Can also be specified via
K8S_AUTH_VERIFY_SSL environment variable."
type: bool
aliases: [ verify_ssl ]
namespaces:
description:
- List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized
to access.
suboptions:
name:
description:
- Optional name to assign to the cluster. If not provided, a name is constructed from the server
and port.
kubeconfig:
description:
- Path to an existing Kubernetes config file. If not provided, and no other connection
options are provided, the OpenShift client will attempt to load the default
configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG
environment variable.
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment
variable.
host:
description:
- Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.
api_key:
description:
- Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment
variable.
username:
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME
environment variable.
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD
environment variable.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment variable.
aliases: [ cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE
environment variable.
aliases: [ key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API. Can also be specified via
K8S_AUTH_SSL_CA_CERT environment variable.
aliases: [ ssl_ca_cert ]
validate_certs:
description:
- "Whether or not to verify the API server's SSL certificates. Can also be specified via
K8S_AUTH_VERIFY_SSL environment variable."
type: bool
aliases: [ verify_ssl ]
namespaces:
description:
- List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized
to access.
requirements:
- "python >= 2.7"
@@ -93,20 +94,20 @@ EXAMPLES = '''
# File must be named k8s.yaml or k8s.yml
# Authenticate with token, and return all pods and services for all namespaces
plugin: k8s
plugin: community.kubernetes.k8s
connections:
- host: https://192.168.64.4:8443
token: xxxxxxxxxxxxxxxx
api_key: xxxxxxxxxxxxxxxx
validate_certs: false
# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
plugin: k8s
plugin: community.kubernetes.k8s
connections:
- namespaces:
- testing
# Use a custom config file, and a specific context.
plugin: k8s
plugin: community.kubernetes.k8s
connections:
- kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer'
@@ -142,6 +143,7 @@ class K8sInventoryException(Exception):
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin):
NAME = 'community.kubernetes.k8s'
connection_plugin = 'kubectl'
transport = 'kubectl'
def parse(self, inventory, loader, path, cache=True):
@@ -275,7 +277,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
self.inventory.set_variable(container_name, 'container_state', 'Waiting')
self.inventory.set_variable(container_name, 'container_ready', container.ready)
self.inventory.set_variable(container_name, 'ansible_remote_tmp', '/tmp/')
self.inventory.set_variable(container_name, 'ansible_connection', self.transport)
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),
@@ -316,7 +318,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
try:
self.inventory.add_child(namespace_services_group, service_name)
except AnsibleError as e:
except AnsibleError:
raise
ports = [{'name': port.name,

View File

@@ -28,60 +28,61 @@ DOCUMENTATION = '''
- Optional list of cluster connection settings. If no connections are provided, the default
I(~/.kube/config) and active context will be used, and objects will be returned for all namespaces
the active user is authorized to access.
name:
description:
- Optional name to assign to the cluster. If not provided, a name is constructed from the server
and port.
kubeconfig:
description:
- Path to an existing Kubernetes config file. If not provided, and no other connection
options are provided, the OpenShift client will attempt to load the default
configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG
environment variable.
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment
variable.
host:
description:
- Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.
api_key:
description:
- Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment
variable.
username:
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME
environment variable.
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD
environment variable.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment variable.
aliases: [ cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE
environment variable.
aliases: [ key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API. Can also be specified via
K8S_AUTH_SSL_CA_CERT environment variable.
aliases: [ ssl_ca_cert ]
validate_certs:
description:
- "Whether or not to verify the API server's SSL certificates. Can also be specified via
K8S_AUTH_VERIFY_SSL environment variable."
type: bool
aliases: [ verify_ssl ]
namespaces:
description:
- List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized
to access.
suboptions:
name:
description:
- Optional name to assign to the cluster. If not provided, a name is constructed from the server
and port.
kubeconfig:
description:
- Path to an existing Kubernetes config file. If not provided, and no other connection
options are provided, the OpenShift client will attempt to load the default
configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG
environment variable.
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment
variable.
host:
description:
- Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.
api_key:
description:
- Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment
variable.
username:
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME
environment variable.
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD
environment variable.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment variable.
aliases: [ cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE
environment variable.
aliases: [ key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API. Can also be specified via
K8S_AUTH_SSL_CA_CERT environment variable.
aliases: [ ssl_ca_cert ]
validate_certs:
description:
- "Whether or not to verify the API server's SSL certificates. Can also be specified via
K8S_AUTH_VERIFY_SSL environment variable."
type: bool
aliases: [ verify_ssl ]
namespaces:
description:
- List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized
to access.
requirements:
- "python >= 2.7"
@@ -93,20 +94,20 @@ EXAMPLES = '''
# File must be named openshift.yaml or openshift.yml
# Authenticate with token, and return all pods and services for all namespaces
plugin: openshift
plugin: community.kubernetes.openshift
connections:
- host: https://192.168.64.4:8443
api_key: xxxxxxxxxxxxxxxx
verify_ssl: false
# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
plugin: openshift
plugin: community.kubernetes.openshift
connections:
- namespaces:
- testing
# Use a custom config file, and a specific context.
plugin: openshift
plugin: community.kubernetes.openshift
connections:
- kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer'

View File

@@ -133,23 +133,23 @@ DOCUMENTATION = '''
EXAMPLES = """
- name: Fetch a list of namespaces
set_fact:
projects: "{{ lookup('k8s', api_version='v1', kind='Namespace') }}"
projects: "{{ lookup('community.kubernetes.k8s', api_version='v1', kind='Namespace') }}"
- name: Fetch all deployments
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment') }}"
deployments: "{{ lookup('community.kubernetes.k8s', kind='Deployment') }}"
- name: Fetch all deployments in a namespace
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment', namespace='testing') }}"
deployments: "{{ lookup('community.kubernetes.k8s', kind='Deployment', namespace='testing') }}"
- name: Fetch a specific deployment by name
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment', namespace='testing', resource_name='elastic') }}"
deployments: "{{ lookup('community.kubernetes.k8s', kind='Deployment', namespace='testing', resource_name='elastic') }}"
- name: Fetch with label selector
set_fact:
service: "{{ lookup('k8s', kind='Service', label_selector='app=galaxy') }}"
service: "{{ lookup('community.kubernetes.k8s', kind='Service', label_selector='app=galaxy') }}"
# Use parameters from a YAML config
@@ -159,11 +159,11 @@ EXAMPLES = """
- name: Using the config (loaded from a file in prior task), fetch the latest version of the object
set_fact:
service: "{{ lookup('k8s', resource_definition=config) }}"
service: "{{ lookup('community.kubernetes.k8s', resource_definition=config) }}"
- name: Use a config from the local filesystem
set_fact:
service: "{{ lookup('k8s', src='service.yml') }}"
service: "{{ lookup('community.kubernetes.k8s', src='service.yml') }}"
"""
RETURN = """
@@ -194,15 +194,14 @@ RETURN = """
type: complex
"""
from ansible.errors import AnsibleError
from ansible.module_utils.common._collections_compat import KeysView
from ansible.plugins.lookup import LookupBase
from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin
from ansible.errors import AnsibleError
try:
from openshift.dynamic import DynamicClient
from openshift.dynamic.exceptions import NotFoundError
HAS_K8S_MODULE_HELPER = True
k8s_import_exception = None
@@ -210,12 +209,6 @@ except ImportError as e:
HAS_K8S_MODULE_HELPER = False
k8s_import_exception = e
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
class KubernetesLookup(K8sAnsibleMixin):
@@ -226,11 +219,6 @@ class KubernetesLookup(K8sAnsibleMixin):
"Requires the OpenShift Python client. Try `pip install openshift`. Detail: {0}".format(k8s_import_exception)
)
if not HAS_YAML:
raise Exception(
"Requires PyYAML. Try `pip install PyYAML`"
)
self.kind = None
self.name = None
self.namespace = None
@@ -253,6 +241,8 @@ class KubernetesLookup(K8sAnsibleMixin):
if cluster_info == 'version':
return [self.client.version]
if cluster_info == 'api_groups':
if isinstance(self.client.resources.api_groups, KeysView):
return [list(self.client.resources.api_groups)]
return [self.client.resources.api_groups]
self.kind = kwargs.get('kind')

View File

@@ -18,14 +18,13 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import copy
import json
from datetime import datetime
import time
import os
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils._text import to_native
@@ -34,7 +33,7 @@ try:
import kubernetes
import openshift
from openshift.dynamic import DynamicClient
from openshift.dynamic.exceptions import ResourceNotFoundError, ResourceNotUniqueError
from openshift.dynamic.exceptions import ResourceNotFoundError, ResourceNotUniqueError, NotFoundError
HAS_K8S_MODULE_HELPER = True
k8s_import_exception = None
except ImportError as e:
@@ -56,13 +55,14 @@ try:
except ImportError:
pass
try:
from openshift.dynamic.apply import recursive_diff
except ImportError:
from ansible.module_utils.common.dict_transformations import recursive_diff
def list_dict_str(value):
if isinstance(value, list):
return value
elif isinstance(value, dict):
return value
elif isinstance(value, string_types):
if isinstance(value, (list, dict, string_types)):
return value
raise TypeError
@@ -78,6 +78,9 @@ COMMON_ARG_SPEC = {
'type': 'bool',
'default': False,
},
}
RESOURCE_ARG_SPEC = {
'resource_definition': {
'type': list_dict_str,
'aliases': ['definition', 'inline']
@@ -85,6 +88,9 @@ COMMON_ARG_SPEC = {
'src': {
'type': 'path',
},
}
NAME_ARG_SPEC = {
'kind': {},
'name': {},
'namespace': {},
@@ -149,20 +155,15 @@ AUTH_ARG_MAP = {
class K8sAnsibleMixin(object):
_argspec_cache = None
@property
def argspec(self):
"""
Introspect the model properties, and return an Ansible module arg_spec dict.
:return: dict
"""
if self._argspec_cache:
return self._argspec_cache
argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
self._argspec_cache = argument_spec
return self._argspec_cache
def __init__(self, *args, **kwargs):
if not HAS_K8S_MODULE_HELPER:
self.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR,
error=to_native(k8s_import_exception))
self.openshift_version = openshift.__version__
if not HAS_YAML:
self.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', {})
@@ -186,13 +187,19 @@ class K8sAnsibleMixin(object):
# We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
pass
elif auth_set('kubeconfig') or auth_set('context'):
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
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:
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config'))
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
configuration = kubernetes.client.Configuration()
@@ -204,7 +211,10 @@ class K8sAnsibleMixin(object):
setattr(configuration, key, value)
kubernetes.client.Configuration.set_default(configuration)
return DynamicClient(kubernetes.client.ApiClient(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):
for attribute in ['kind', 'name', 'singular_name']:
@@ -258,36 +268,165 @@ class K8sAnsibleMixin(object):
self.fail(msg="Error loading resource_definition: {0}".format(exc))
return result
@staticmethod
def diff_objects(existing, new):
def diff_objects(self, existing, new):
result = dict()
diff = recursive_diff(existing, new)
if diff:
result['before'] = diff[0]
result['after'] = diff[1]
return not diff, result
if not diff:
return True, result
result['before'] = diff[0]
result['after'] = diff[1]
class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
resource_definition = None
api_version = None
kind = None
# If only metadata.generation and metadata.resourceVersion changed, ignore it
ignored_keys = set(['generation', 'resourceVersion'])
def __init__(self, *args, **kwargs):
if list(result['after'].keys()) != ['metadata'] or list(result['before'].keys()) != ['metadata']:
return False, result
kwargs['argument_spec'] = self.argspec
AnsibleModule.__init__(self, *args, **kwargs)
if not set(result['after']['metadata'].keys()).issubset(ignored_keys):
return False, result
if not set(result['before']['metadata'].keys()).issubset(ignored_keys):
return False, result
if not HAS_K8S_MODULE_HELPER:
self.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR,
error=to_native(k8s_import_exception))
self.openshift_version = openshift.__version__
if hasattr(self, 'warn'):
self.warn('No meaningful diff was generated, but the API may not be idempotent (only metadata.generation or metadata.resourceVersion were changed)')
if not HAS_YAML:
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
def execute_module(self):
raise NotImplementedError()
return True, result
def fail(self, msg=None):
self.fail_json(msg=msg)
def _wait_for(self, resource, name, namespace, predicate, sleep, timeout, state):
start = datetime.now()
def _wait_for_elapsed():
return (datetime.now() - start).seconds
response = None
while _wait_for_elapsed() < timeout:
try:
response = resource.get(name=name, namespace=namespace)
if predicate(response):
if response:
return True, response.to_dict(), _wait_for_elapsed()
else:
return True, {}, _wait_for_elapsed()
time.sleep(sleep)
except NotFoundError:
if state == 'absent':
return True, {}, _wait_for_elapsed()
if response:
response = response.to_dict()
return False, response, _wait_for_elapsed()
def wait(self, resource, definition, sleep, timeout, state='present', condition=None):
def _deployment_ready(deployment):
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
# Furthermore deployment.status.availableReplicas == deployment.status.replicas == None if status is empty
# deployment.status.replicas is None is perfectly ok if desired replicas == 0
# Scaling up means that we also need to check that we're not in a
# situation where status.replicas == status.availableReplicas
# but spec.replicas != status.replicas
return (deployment.status
and deployment.spec.replicas == (deployment.status.replicas or 0)
and deployment.status.availableReplicas == deployment.status.replicas
and deployment.status.observedGeneration == deployment.metadata.generation
and not deployment.status.unavailableReplicas)
def _pod_ready(pod):
return (pod.status and pod.status.containerStatuses is not None
and all([container.ready for container in pod.status.containerStatuses]))
def _daemonset_ready(daemonset):
return (daemonset.status and daemonset.status.desiredNumberScheduled is not None
and daemonset.status.numberReady == daemonset.status.desiredNumberScheduled
and daemonset.status.observedGeneration == daemonset.metadata.generation
and not daemonset.status.unavailableReplicas)
def _custom_condition(resource):
if not resource.status or not resource.status.conditions:
return False
match = [x for x in resource.status.conditions if x.type == condition['type']]
if not match:
return False
# There should never be more than one condition of a specific type
match = match[0]
if match.status == 'Unknown':
if match.status == condition['status']:
if 'reason' not in condition:
return True
if condition['reason']:
return match.reason == condition['reason']
return False
status = True if match.status == 'True' else False
if status == condition['status']:
if condition.get('reason'):
return match.reason == condition['reason']
return True
return False
def _resource_absent(resource):
return not resource
waiter = dict(
Deployment=_deployment_ready,
DaemonSet=_daemonset_ready,
Pod=_pod_ready
)
kind = definition['kind']
if state == 'present' and not condition:
predicate = waiter.get(kind, lambda x: x)
elif state == 'present' and condition:
predicate = _custom_condition
else:
predicate = _resource_absent
return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state)
def set_resource_definitions(self):
resource_definition = self.params.get('resource_definition')
self.resource_definitions = []
if resource_definition:
if isinstance(resource_definition, string_types):
try:
self.resource_definitions = yaml.safe_load_all(resource_definition)
except (IOError, yaml.YAMLError) as exc:
self.fail(msg="Error loading resource_definition: {0}".format(exc))
elif isinstance(resource_definition, list):
self.resource_definitions = resource_definition
else:
self.resource_definitions = [resource_definition]
src = self.params.get('src')
if src:
self.resource_definitions = self.load_resource_definitions(src)
try:
self.resource_definitions = [item for item in self.resource_definitions if item]
except AttributeError:
pass
if not resource_definition and not src:
implicit_definition = dict(
kind=self.kind,
apiVersion=self.api_version,
metadata=dict(name=self.name)
)
if self.namespace:
implicit_definition['metadata']['namespace'] = self.namespace
self.resource_definitions = [implicit_definition]
class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
# NOTE: This class KubernetesAnsibleModule is deprecated in favor of
# class K8sAnsibleMixin and will be removed 2.0.0 release.
# Please use K8sAnsibleMixin instead.
def __init__(self, *args, **kwargs):
kwargs['argument_spec'] = self.argspec
AnsibleModule.__init__(self, *args, **kwargs)
K8sAnsibleMixin.__init__(self, *args, **kwargs)
self.warn("class KubernetesAnsibleModule is deprecated"
" and will be removed in 2.0.0. Please use K8sAnsibleMixin instead.")

View File

@@ -20,32 +20,24 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
import copy
from datetime import datetime
from distutils.version import LooseVersion
import time
import sys
import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible.module_utils.six import string_types
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule
from ansible.module_utils.basic import missing_required_lib, AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC, K8sAnsibleMixin)
try:
import yaml
from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError, ForbiddenError, KubernetesValidateMissing
import urllib3
except ImportError:
# Exceptions handled in common
pass
try:
import kubernetes_validate
HAS_KUBERNETES_VALIDATE = True
except ImportError:
HAS_KUBERNETES_VALIDATE = False
K8S_CONFIG_HASH_IMP_ERR = None
try:
@@ -63,7 +55,7 @@ except ImportError:
HAS_K8S_APPLY = False
class KubernetesRawModule(KubernetesAnsibleModule):
class KubernetesRawModule(K8sAnsibleMixin):
@property
def validate_spec(self):
@@ -84,6 +76,8 @@ class KubernetesRawModule(KubernetesAnsibleModule):
@property
def argspec(self):
argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
argument_spec.update(copy.deepcopy(NAME_ARG_SPEC))
argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC))
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge'])
argument_spec['wait'] = dict(type='bool', default=False)
@@ -104,15 +98,25 @@ class KubernetesRawModule(KubernetesAnsibleModule):
('merge_type', 'apply'),
]
KubernetesAnsibleModule.__init__(self, *args,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
**kwargs)
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(KubernetesRawModule, 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')
resource_definition = self.params.get('resource_definition')
validate = self.params.get('validate')
if validate:
if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"):
@@ -129,34 +133,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
if self.apply:
if not HAS_K8S_APPLY:
self.fail_json(msg=missing_required_lib("openshift >= 0.9.2", reason="for apply"))
if resource_definition:
if isinstance(resource_definition, string_types):
try:
self.resource_definitions = yaml.safe_load_all(resource_definition)
except (IOError, yaml.YAMLError) as exc:
self.fail(msg="Error loading resource_definition: {0}".format(exc))
elif isinstance(resource_definition, list):
self.resource_definitions = resource_definition
else:
self.resource_definitions = [resource_definition]
src = self.params.get('src')
if src:
self.resource_definitions = self.load_resource_definitions(src)
try:
self.resource_definitions = [item for item in self.resource_definitions if item]
except AttributeError:
pass
if not resource_definition and not src:
implicit_definition = dict(
kind=self.kind,
apiVersion=self.api_version,
metadata=dict(name=self.name)
)
if self.namespace:
implicit_definition['metadata']['namespace'] = self.namespace
self.resource_definitions = [implicit_definition]
self.set_resource_definitions()
def flatten_list_kind(self, list_resource, definitions):
flattened = []
@@ -178,9 +155,11 @@ class KubernetesRawModule(KubernetesAnsibleModule):
flattened_definitions = []
for definition in self.resource_definitions:
if definition is None:
continue
kind = definition.get('kind', self.kind)
api_version = definition.get('apiVersion', self.api_version)
if kind.endswith('List'):
if kind and kind.endswith('List'):
resource = self.find_resource(kind, api_version, fail=False)
flattened_definitions.extend(self.flatten_list_kind(resource, definition))
else:
@@ -274,6 +253,9 @@ class KubernetesRawModule(KubernetesAnsibleModule):
except DynamicApiError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
except Exception as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(to_native(exc)),
error='', status='', reason='')
if state == 'absent':
result['method'] = "delete"
@@ -299,7 +281,11 @@ class KubernetesRawModule(KubernetesAnsibleModule):
else:
if self.apply:
if self.check_mode:
ignored, k8s_obj = apply_object(resource, definition)
ignored, patch = apply_object(resource, definition)
if existing:
k8s_obj = dict_merge(existing.to_dict(), patch)
else:
k8s_obj = patch
else:
try:
k8s_obj = resource.apply(definition, namespace=namespace).to_dict()
@@ -310,7 +296,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
success = True
result['result'] = k8s_obj
if wait:
if wait and not self.check_mode:
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
if existing:
existing = existing.to_dict()
@@ -369,7 +355,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
success = True
result['result'] = k8s_obj
if wait:
if wait and not self.check_mode:
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
match, diffs = self.diff_objects(existing.to_dict(), result['result'])
result['changed'] = not match
@@ -397,7 +383,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
success = True
result['result'] = k8s_obj
if wait:
if wait and not self.check_mode:
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
match, diffs = self.diff_objects(existing.to_dict(), result['result'])
result['changed'] = not match
@@ -428,6 +414,12 @@ class KubernetesRawModule(KubernetesAnsibleModule):
msg += "\n" + "\n ".join(self.warnings)
error = dict(msg=msg, error=exc.status, status=exc.status, reason=exc.reason, warnings=self.warnings)
return None, error
except Exception as exc:
msg = "Failed to patch object: {0}".format(exc)
if self.warnings:
msg += "\n" + "\n ".join(self.warnings)
error = dict(msg=msg, error=to_native(exc), status='', reason='', warnings=self.warnings)
return None, error
def create_project_request(self, definition):
definition['kind'] = 'ProjectRequest'
@@ -443,83 +435,3 @@ class KubernetesRawModule(KubernetesAnsibleModule):
result['changed'] = True
result['method'] = 'create'
return result
def _wait_for(self, resource, name, namespace, predicate, sleep, timeout, state):
start = datetime.now()
def _wait_for_elapsed():
return (datetime.now() - start).seconds
response = None
while _wait_for_elapsed() < timeout:
try:
response = resource.get(name=name, namespace=namespace)
if predicate(response):
if response:
return True, response.to_dict(), _wait_for_elapsed()
else:
return True, {}, _wait_for_elapsed()
time.sleep(sleep)
except NotFoundError:
if state == 'absent':
return True, {}, _wait_for_elapsed()
if response:
response = response.to_dict()
return False, response, _wait_for_elapsed()
def wait(self, resource, definition, sleep, timeout, state='present', condition=None):
def _deployment_ready(deployment):
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
# Furthermore deployment.status.availableReplicas == deployment.status.replicas == None if status is empty
return (deployment.status and deployment.status.replicas is not None and
deployment.status.availableReplicas == deployment.status.replicas and
deployment.status.observedGeneration == deployment.metadata.generation)
def _pod_ready(pod):
return (pod.status and pod.status.containerStatuses is not None and
all([container.ready for container in pod.status.containerStatuses]))
def _daemonset_ready(daemonset):
return (daemonset.status and daemonset.status.desiredNumberScheduled is not None and
daemonset.status.numberReady == daemonset.status.desiredNumberScheduled and
daemonset.status.observedGeneration == daemonset.metadata.generation)
def _custom_condition(resource):
if not resource.status or not resource.status.conditions:
return False
match = [x for x in resource.status.conditions if x.type == condition['type']]
if not match:
return False
# There should never be more than one condition of a specific type
match = match[0]
if match.status == 'Unknown':
if match.status == condition['status']:
if 'reason' not in condition:
return True
if condition['reason']:
return match.reason == condition['reason']
return False
status = True if match.status == 'True' else False
if status == condition['status']:
if condition.get('reason'):
return match.reason == condition['reason']
return True
return False
def _resource_absent(resource):
return not resource
waiter = dict(
Deployment=_deployment_ready,
DaemonSet=_daemonset_ready,
Pod=_pod_ready
)
kind = definition['kind']
if state == 'present' and not condition:
predicate = waiter.get(kind, lambda x: x)
elif state == 'present' and condition:
predicate = _custom_condition
else:
predicate = _resource_absent
return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state)

View File

@@ -20,21 +20,15 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
import copy
import math
import time
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule
from ansible.module_utils.six import string_types
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:
import yaml
from openshift import watch
from openshift.dynamic.client import ResourceInstance
from openshift.helper.exceptions import KubernetesException
except ImportError as exc:
class KubernetesException(Exception):
pass
from openshift.dynamic.exceptions import NotFoundError
except ImportError:
pass
SCALE_ARG_SPEC = {
@@ -46,7 +40,7 @@ SCALE_ARG_SPEC = {
}
class KubernetesAnsibleScaleModule(KubernetesAnsibleModule):
class KubernetesAnsibleScaleModule(K8sAnsibleMixin):
def __init__(self, k8s_kind=None, *args, **kwargs):
self.client = None
@@ -56,39 +50,25 @@ class KubernetesAnsibleScaleModule(KubernetesAnsibleModule):
('resource_definition', 'src'),
]
KubernetesAnsibleModule.__init__(self, *args,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
**kwargs)
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')
resource_definition = self.params.get('resource_definition')
if resource_definition:
if isinstance(resource_definition, string_types):
try:
self.resource_definitions = yaml.safe_load_all(resource_definition)
except (IOError, yaml.YAMLError) as exc:
self.fail(msg="Error loading resource_definition: {0}".format(exc))
elif isinstance(resource_definition, list):
self.resource_definitions = resource_definition
else:
self.resource_definitions = [resource_definition]
src = self.params.get('src')
if src:
self.resource_definitions = self.load_resource_definitions(src)
if not resource_definition and not src:
implicit_definition = dict(
kind=self.kind,
apiVersion=self.api_version,
metadata=dict(name=self.name)
)
if self.namespace:
implicit_definition['metadata']['namespace'] = self.namespace
self.resource_definitions = [implicit_definition]
self.set_resource_definitions()
def execute_module(self):
definition = self.resource_definitions[0]
@@ -107,14 +87,16 @@ class KubernetesAnsibleScaleModule(KubernetesAnsibleModule):
wait_time = self.params.get('wait_timeout')
existing = None
existing_count = None
return_attributes = dict(changed=False, result=dict())
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 KubernetesException as exc:
except NotFoundError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
error=exc.value.get('status'))
@@ -137,108 +119,48 @@ class KubernetesAnsibleScaleModule(KubernetesAnsibleModule):
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
k8s_obj = resource.patch(existing.to_dict())
return_attributes['result'] = resource.patch(existing.to_dict()).to_dict()
else:
k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
return_attributes['result'] = k8s_obj.to_dict()
return_attributes = self.scale(resource, existing, replicas, wait, wait_time)
self.exit_json(**return_attributes)
@property
def argspec(self):
args = copy.deepcopy(COMMON_ARG_SPEC)
args.pop('state')
args.pop('force')
args = copy.deepcopy(SCALE_ARG_SPEC)
args.update(RESOURCE_ARG_SPEC)
args.update(NAME_ARG_SPEC)
args.update(AUTH_ARG_SPEC)
args.update(SCALE_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 = {'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
return_obj = None
stream = None
if wait:
w, stream = self._create_stream(resource, namespace, wait_time)
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)
)
self.fail_json(msg="Scale request failed: {0}".format(exc))
if wait and stream is not None:
return_obj = self._read_stream(resource, w, stream, name, replicas)
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 not return_obj:
return_obj = self._wait_for_response(resource, name, namespace)
return return_obj
def _create_stream(self, resource, namespace, wait_time):
""" Create a stream of events for the object """
w = None
stream = None
try:
w = watch.Watch()
w._api_client = self.client.client
if namespace:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
except KubernetesException:
pass
return w, stream
def _read_stream(self, resource, watcher, stream, name, replicas):
""" Wait for ready_replicas to equal the requested number of replicas. """
return_obj = None
try:
for event in stream:
if event.get('object'):
obj = ResourceInstance(resource, event['object'])
if obj.metadata.name == name and hasattr(obj, 'status'):
if replicas == 0:
if not hasattr(obj.status, 'readyReplicas') or not obj.status.readyReplicas:
return_obj = obj
watcher.stop()
break
if hasattr(obj.status, 'readyReplicas') and obj.status.readyReplicas == replicas:
return_obj = obj
watcher.stop()
break
except Exception as exc:
self.fail_json(msg="Exception reading event stream: {0}".format(exc))
if not return_obj:
self.fail_json(msg="Error fetching the patched object. Try a higher wait_timeout value.")
if replicas and return_obj.status.readyReplicas is None:
self.fail_json(msg="Failed to fetch the number of ready replicas. Try a higher wait_timeout value.")
if replicas and return_obj.status.readyReplicas != replicas:
self.fail_json(msg="Number of ready replicas is {0}. Failed to reach {1} ready replicas within "
"the wait_timeout period.".format(return_obj.status.ready_replicas, replicas))
return return_obj
def _wait_for_response(self, resource, name, namespace):
""" Wait for an API response """
tries = 0
half = math.ceil(20 / 2)
obj = None
while tries <= half:
obj = resource.get(name=name, namespace=namespace)
if obj:
break
tries += 2
time.sleep(2)
return obj
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

@@ -6,16 +6,15 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
---
module: helm
short_description: Manages Kubernetes packages with the Helm package manager
version_added: "0.11.0"
author:
- Lucas Boisserie (@LucasBoisserie)
- Matthieu Diehr (@d-matt)
@@ -28,11 +27,6 @@ description:
- Install, upgrade, delete packages with the Helm package manager.
options:
binary_path:
description:
- The path of a helm binary to use.
required: false
type: path
chart_ref:
description:
- chart_reference on chart repository.
@@ -96,15 +90,6 @@ options:
- Helm option to force reinstall, ignore on new install.
default: False
type: bool
kube_context:
description:
- Helm option to specify which kubeconfig context to use.
type: str
kubeconfig_path:
description:
- Helm option to specify kubeconfig path to use.
type: path
aliases: [ kubeconfig ]
purge:
description:
- Remove the release from the store and make its name free for later use.
@@ -119,24 +104,44 @@ options:
description:
- Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration).
type: str
atomic:
description:
- If set, the installation process deletes the installation on failure.
type: bool
default: False
create_namespace:
description:
- Create the release namespace if not present.
type: bool
default: False
version_added: "0.11.1"
replace:
description:
- Reuse the given name, only if that name is a deleted release which remains in the history.
- This is unsafe in production environment.
type: bool
default: False
version_added: "1.11.0"
extends_documentation_fragment:
- community.kubernetes.helm_common_options
'''
EXAMPLES = '''
- name: Create helm namespace as HELM 3 doesn't create it automatically
k8s:
api_version: v1
kind: Namespace
name: "monitoring"
wait: true
EXAMPLES = r'''
- name: Deploy latest version of Prometheus chart inside monitoring namespace (and create it)
community.kubernetes.helm:
name: test
chart_ref: stable/prometheus
release_namespace: monitoring
create_namespace: true
# From repository
- name: Add stable chart repo
helm_repository:
community.kubernetes.helm_repository:
name: stable
repo_url: "https://kubernetes-charts.storage.googleapis.com"
- name: Deploy latest version of Grafana chart inside monitoring namespace with values
helm:
community.kubernetes.helm:
name: test
chart_ref: stable/grafana
release_namespace: monitoring
@@ -144,39 +149,39 @@ EXAMPLES = '''
replicas: 2
- name: Deploy Grafana chart on 5.0.12 with values loaded from template
helm:
community.kubernetes.helm:
name: test
chart_ref: stable/grafana
chart_version: 5.0.12
values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}"
- name: Remove test release and waiting suppression ending
helm:
community.kubernetes.helm:
name: test
state: absent
wait: true
# From git
- name: Git clone stable repo on HEAD
git:
ansible.builtin.git:
repo: "http://github.com/helm/charts.git"
dest: /tmp/helm_repo
- name: Deploy Grafana chart from local path
helm:
community.kubernetes.helm:
name: test
chart_ref: /tmp/helm_repo/stable/grafana
release_namespace: monitoring
# From url
- name: Deploy Grafana chart on 5.0.12 from url
helm:
community.kubernetes.helm:
name: test
chart_ref: "https://kubernetes-charts.storage.googleapis.com/grafana-5.0.12.tgz"
release_namespace: monitoring
'''
RETURN = """
RETURN = r"""
status:
type: complex
description: A dictionary of status output
@@ -231,6 +236,7 @@ command:
sample: helm upgrade ...
"""
import tempfile
import traceback
try:
@@ -240,32 +246,42 @@ except ImportError:
IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
module = None
# Get Values from deployed release
def get_values(command, release_name):
get_command = command + " get values --output=yaml " + release_name
rc, out, err = module.run_command(get_command)
def exec_command(command):
rc, out, err = module.run_command(command)
if rc != 0:
module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
command=get_command
stdout=out,
stderr=err,
command=command,
)
return rc, out, err
def get_values(command, release_name):
"""
Get Values from deployed release
"""
get_command = command + " get values --output=yaml " + release_name
rc, out, err = exec_command(get_command)
# Helm 3 return "null" string when no values are set
if out.rstrip("\n") == "null":
return {}
else:
return yaml.safe_load(out)
return yaml.safe_load(out)
# Get Release from all deployed releases
def get_release(state, release_name):
"""
Get Release from all deployed releases
"""
if state is not None:
for release in state:
if release['name'] == release_name:
@@ -273,17 +289,14 @@ def get_release(state, release_name):
return None
# Get Release state from deployed release
def get_release_status(command, release_name):
"""
Get Release state from deployed release
"""
list_command = command + " list --output=yaml --filter " + release_name
rc, out, err = module.run_command(list_command)
if rc != 0:
module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
command=list_command
)
rc, out, err = exec_command(list_command)
release = get_release(yaml.safe_load(out), release_name)
@@ -295,56 +308,61 @@ def get_release_status(command, release_name):
return release
# Run Repo update
def run_repo_update(command):
"""
Run Repo update
"""
repo_update_command = command + " repo update"
rc, out, err = module.run_command(repo_update_command)
if rc != 0:
module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
command=repo_update_command
)
rc, out, err = exec_command(repo_update_command)
# Get chart info
def fetch_chart_info(command, chart_ref):
"""
Get chart info
"""
inspect_command = command + " show chart " + chart_ref
rc, out, err = module.run_command(inspect_command)
if rc != 0:
module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
command=inspect_command
)
rc, out, err = exec_command(inspect_command)
return yaml.safe_load(out)
# Install/upgrade/rollback release chart
def deploy(command, release_name, release_values, chart_name, wait, wait_timeout, disable_hook, force):
deploy_command = command + " upgrade -i" # install/upgrade
def deploy(command, release_name, release_values, chart_name, wait,
wait_timeout, disable_hook, force, atomic=False, create_namespace=False,
replace=False):
"""
Install/upgrade/rollback release chart
"""
if replace:
# '--replace' is not supported by 'upgrade -i'
deploy_command = command + " install"
else:
deploy_command = command + " upgrade -i" # install/upgrade
# Always reset values to keep release_values equal to values released
deploy_command += " --reset-values"
# Always reset values to keep release_values equal to values released
deploy_command += " --reset-values"
if wait:
deploy_command += " --wait"
if wait_timeout is not None:
deploy_command += " --timeout " + wait_timeout
if atomic:
deploy_command += " --atomic"
if force:
deploy_command += " --force"
if replace:
deploy_command += " --replace"
if disable_hook:
deploy_command += " --no-hooks"
if release_values != {}:
try:
import tempfile
except ImportError:
module.fail_json(msg=missing_required_lib("tempfile"), exception=traceback.format_exc())
if create_namespace:
deploy_command += " --create-namespace"
if release_values != {}:
fd, path = tempfile.mkstemp(suffix='.yml')
with open(path, 'w') as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False)
@@ -355,8 +373,11 @@ def deploy(command, release_name, release_values, chart_name, wait, wait_timeout
return deploy_command
# Delete release chart
def delete(command, release_name, purge, disable_hook):
"""
Delete release chart
"""
delete_command = command + " uninstall "
if not purge:
@@ -387,11 +408,14 @@ def main():
# Helm options
disable_hook=dict(type='bool', default=False),
force=dict(type='bool', default=False),
kube_context=dict(type='str'),
kubeconfig_path=dict(type='path', aliases=['kubeconfig']),
kube_context=dict(type='str', aliases=['context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
kubeconfig_path=dict(type='path', aliases=['kubeconfig'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
purge=dict(type='bool', default=True),
wait=dict(type='bool', default=False),
wait_timeout=dict(type='str'),
atomic=dict(type='bool', default=False),
create_namespace=dict(type='bool', default=False),
replace=dict(type='bool', default=False),
),
required_if=[
('release_state', 'present', ['release_name', 'chart_ref']),
@@ -423,6 +447,9 @@ def main():
purge = module.params.get('purge')
wait = module.params.get('wait')
wait_timeout = module.params.get('wait_timeout')
atomic = module.params.get('atomic')
create_namespace = module.params.get('create_namespace')
replace = module.params.get('replace')
if bin_path is not None:
helm_cmd_common = bin_path
@@ -446,6 +473,9 @@ def main():
# keep helm_cmd_common for get_release_status in module_exit_json
helm_cmd = helm_cmd_common
if release_state == "absent" and release_status is not None:
if replace:
module.fail_json(msg="replace is not applicable when state is absent")
helm_cmd = delete(helm_cmd, release_name, purge, disable_hook)
changed = True
elif release_state == "present":
@@ -461,30 +491,42 @@ def main():
if release_status is None: # Not installed
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
disable_hook, False)
disable_hook, False, atomic=atomic, create_namespace=create_namespace,
replace=replace)
changed = True
elif force or release_values != release_status['values'] \
or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"]:
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
disable_hook, force)
disable_hook, force, atomic=atomic, create_namespace=create_namespace,
replace=replace)
changed = True
if module.check_mode:
module.exit_json(changed=changed)
module.exit_json(
changed=changed,
command=helm_cmd,
stdout='',
stderr='',
)
elif not changed:
module.exit_json(changed=False, status=release_status)
rc, out, err = module.run_command(helm_cmd)
if rc != 0:
module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
command=helm_cmd
module.exit_json(
changed=False,
status=release_status,
stdout='',
stderr='',
command=helm_cmd,
)
module.exit_json(changed=changed, stdout=out, stderr=err,
status=get_release_status(helm_cmd_common, release_name), command=helm_cmd)
rc, out, err = exec_command(helm_cmd)
module.exit_json(
changed=changed,
stdout=out,
stderr=err,
status=get_release_status(helm_cmd_common, release_name),
command=helm_cmd,
)
if __name__ == '__main__':

View File

@@ -1,20 +1,19 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: 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)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
---
module: helm_info
short_description: Get informations from Helm package deployed inside the cluster
short_description: Get information from Helm package deployed inside the cluster
version_added: "0.11.0"
author:
- Lucas Boisserie (@LucasBoisserie)
@@ -24,14 +23,9 @@ requirements:
- "yaml (https://pypi.org/project/PyYAML/)"
description:
- Get informations (values, states, ...) from Helm package deployed inside the cluster
- Get information (values, states, ...) from Helm package deployed inside the cluster.
options:
binary_path:
description:
- The path of a helm binary to use.
required: false
type: path
release_name:
description:
- Release name to manage.
@@ -44,27 +38,18 @@ options:
required: true
type: str
aliases: [ namespace ]
#Helm options
kube_context:
description:
- Helm option to specify which kubeconfig context to use.
type: str
kubeconfig_path:
description:
- Helm option to specify kubeconfig path to use.
type: path
aliases: [ kubeconfig ]
extends_documentation_fragment:
- community.kubernetes.helm_common_options
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Deploy latest version of Grafana chart inside monitoring namespace
helm_info:
community.kubernetes.helm_info:
name: test
release_namespace: monitoring
'''
RETURN = """
RETURN = r'''
status:
type: complex
description: A dictionary of status output
@@ -102,7 +87,7 @@ status:
type: str
returned: always
description: Dict of Values used to deploy
"""
'''
import traceback
@@ -113,7 +98,7 @@ except ImportError:
IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
module = None
@@ -178,8 +163,8 @@ def main():
release_namespace=dict(type='str', required=True, aliases=['namespace']),
# Helm options
kube_context=dict(type='str'),
kubeconfig_path=dict(type='path', aliases=['kubeconfig']),
kube_context=dict(type='str', aliases=['context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
kubeconfig_path=dict(type='path', aliases=['kubeconfig'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
),
supports_check_mode=True,
)
@@ -212,8 +197,8 @@ def main():
if release_status is not None:
module.exit_json(changed=False, status=release_status)
else:
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == '__main__':

View File

@@ -0,0 +1,242 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# 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
__metaclass__ = type
DOCUMENTATION = r'''
---
module: helm_plugin
short_description: Manage Helm plugins
version_added: "1.0.0"
author:
- Abhijeet Kasurde (@Akasurde)
requirements:
- "helm (https://github.com/helm/helm/releases)"
description:
- Install, uninstall Helm plugins.
options:
release_namespace:
description:
- Kubernetes namespace where the helm plugin should be installed.
required: true
type: str
aliases: [ namespace ]
#Helm options
state:
description:
- If C(state=present), Helm plugin will be installed.
- If C(state=absent), Helm plugin will be uninstalled.
choices: [ absent, present ]
default: present
type: str
plugin_name:
description:
- Name of Helm plugin.
- Required only if C(state=absent).
type: str
plugin_path:
description:
- Plugin path to a plugin on your local file system or a url of a remote VCS repo.
- If plugin path from file system is provided, make sure that tar is present on remote
machine and not on Ansible controller.
- Required only if C(state=present).
type: str
extends_documentation_fragment:
- community.kubernetes.helm_common_options
'''
EXAMPLES = r'''
- name: Install Helm env plugin
community.kubernetes.helm_plugin:
plugin_path: https://github.com/adamreese/helm-env
state: present
- name: Install Helm plugin from local filesystem
community.kubernetes.helm_plugin:
plugin_path: https://domain/path/to/plugin.tar.gz
state: present
- name: Uninstall Helm env plugin
community.kubernetes.helm_plugin:
plugin_name: env
state: absent
'''
RETURN = r'''
stdout:
type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log
returned: always
sample: ''
stderr:
type: str
description: Full `helm` command stderr, in case you want to display it or examine the event log
returned: always
sample: ''
command:
type: str
description: Full `helm` command built by this module, in case you want to re-run the command outside the module or debug a problem.
returned: always
sample: helm plugin list ...
msg:
type: str
description: Info about successful command
returned: always
sample: "Plugin installed successfully"
rc:
type: int
description: Helm plugin command return code
returned: always
sample: 1
'''
from ansible.module_utils.basic import AnsibleModule, env_fallback
def main():
module = AnsibleModule(
argument_spec=dict(
binary_path=dict(type='path'),
release_namespace=dict(type='str', required=True, aliases=['namespace']),
state=dict(type='str', default='present', choices=['present', 'absent']),
plugin_path=dict(type='str',),
plugin_name=dict(type='str',),
# Helm options
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
),
supports_check_mode=True,
required_if=[
("state", "present", ("plugin_path",)),
("state", "absent", ("plugin_name",)),
],
mutually_exclusive=[
['plugin_name', 'plugin_path'],
],
)
bin_path = module.params.get('binary_path')
release_namespace = module.params.get('release_namespace')
state = module.params.get('state')
# Helm options
kube_context = module.params.get('context')
kubeconfig_path = module.params.get('kubeconfig')
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = 'helm'
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
helm_cmd_common += " plugin"
if kube_context is not None:
helm_cmd_common += " --kube-context " + kube_context
if kubeconfig_path is not None:
helm_cmd_common += " --kubeconfig " + kubeconfig_path
helm_cmd_common += " --namespace=" + release_namespace
if state == 'present':
helm_cmd_common += " install %s" % module.params.get('plugin_path')
if not module.check_mode:
rc, out, err = module.run_command(helm_cmd_common)
else:
rc, out, err = (0, '', '')
if rc == 1 and 'plugin already exists' in err:
module.exit_json(
failed=False,
changed=False,
msg="Plugin already exists",
command=helm_cmd_common,
stdout=out,
stderr=err,
rc=rc
)
elif rc == 0:
module.exit_json(
failed=False,
changed=True,
msg="Plugin installed successfully",
command=helm_cmd_common,
stdout=out,
stderr=err,
rc=rc,
)
else:
module.fail_json(
msg="Failure when executing Helm command.",
command=helm_cmd_common,
stdout=out,
stderr=err,
rc=rc,
)
elif state == 'absent':
plugin_name = module.params.get('plugin_name')
helm_plugin_list = helm_cmd_common + " list"
rc, out, err = module.run_command(helm_plugin_list)
if rc != 0 or (out == '' and err == ''):
module.fail_json(
msg="Failed to get Helm plugin info",
command=helm_plugin_list,
stdout=out,
stderr=err,
rc=rc,
)
if out:
found = False
for line in out.splitlines():
if line.startswith("NAME"):
continue
name, dummy, dummy = line.split('\t', 3)
name = name.strip()
if name == plugin_name:
found = True
break
if found:
helm_uninstall_cmd = "%s uninstall %s" % (helm_cmd_common, plugin_name)
if not module.check_mode:
rc, out, err = module.run_command(helm_uninstall_cmd)
else:
rc, out, err = (0, '', '')
if rc == 0:
module.exit_json(
changed=True,
msg="Plugin uninstalled successfully",
command=helm_uninstall_cmd,
stdout=out,
stderr=err,
rc=rc
)
module.fail_json(
msg="Failed to get Helm plugin uninstall",
command=helm_uninstall_cmd,
stdout=out,
stderr=err,
rc=rc,
)
else:
module.exit_json(
failed=False,
changed=False,
msg="Plugin not found or is already uninstalled",
command=helm_plugin_list,
stdout=out,
stderr=err,
rc=rc
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,167 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# 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
__metaclass__ = type
DOCUMENTATION = r'''
---
module: helm_plugin_info
short_description: Gather information about Helm plugins
version_added: "1.0.0"
author:
- Abhijeet Kasurde (@Akasurde)
requirements:
- "helm (https://github.com/helm/helm/releases)"
description:
- Gather information about Helm plugins installed in namespace.
options:
release_namespace:
description:
- Kubernetes namespace where the helm plugins are installed.
required: true
type: str
aliases: [ namespace ]
#Helm options
plugin_name:
description:
- Name of Helm plugin, to gather particular plugin info.
type: str
extends_documentation_fragment:
- community.kubernetes.helm_common_options
'''
EXAMPLES = r'''
- name: Gather Helm plugin info
community.kubernetes.helm_plugin_info:
- name: Gather Helm plugin info
community.kubernetes.helm_plugin_info:
plugin_name: env
'''
RETURN = r'''
stdout:
type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log
returned: always
sample: ''
stderr:
type: str
description: Full `helm` command stderr, in case you want to display it or examine the event log
returned: always
sample: ''
command:
type: str
description: Full `helm` command built by this module, in case you want to re-run the command outside the module or debug a problem.
returned: always
sample: helm plugin list ...
plugin_list:
type: list
description: Helm plugin dict inside a list
returned: always
sample: {
"name": "env",
"version": "0.1.0",
"description": "Print out the helm environment."
}
rc:
type: int
description: Helm plugin command return code
returned: always
sample: 1
'''
from ansible.module_utils.basic import AnsibleModule, env_fallback
def main():
module = AnsibleModule(
argument_spec=dict(
binary_path=dict(type='path'),
release_namespace=dict(type='str', required=True, aliases=['namespace']),
plugin_name=dict(type='str',),
# Helm options
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
),
supports_check_mode=True,
)
bin_path = module.params.get('binary_path')
release_namespace = module.params.get('release_namespace')
# Helm options
kube_context = module.params.get('context')
kubeconfig_path = module.params.get('kubeconfig')
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = 'helm'
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
helm_cmd_common += " plugin"
if kube_context is not None:
helm_cmd_common += " --kube-context " + kube_context
if kubeconfig_path is not None:
helm_cmd_common += " --kubeconfig " + kubeconfig_path
helm_cmd_common += " --namespace=" + release_namespace
plugin_name = module.params.get('plugin_name')
helm_plugin_list = helm_cmd_common + " list"
rc, out, err = module.run_command(helm_plugin_list)
if rc != 0 or (out == '' and err == ''):
module.fail_json(
msg="Failed to get Helm plugin info",
command=helm_plugin_list,
stdout=out,
stderr=err,
rc=rc,
)
plugin_list = []
if out:
for line in out.splitlines():
if line.startswith("NAME"):
continue
name, version, description = line.split('\t', 3)
name = name.strip()
version = version.strip()
description = description.strip()
if plugin_name is None:
plugin_list.append({
'name': name,
'version': version,
'description': description,
})
continue
if plugin_name == name:
plugin_list.append({
'name': name,
'version': version,
'description': description,
})
break
module.exit_json(
changed=True,
command=helm_plugin_list,
stdout=out,
stderr=err,
rc=rc,
plugin_list=plugin_list,
)
if __name__ == '__main__':
main()

View File

@@ -1,21 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: 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)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
---
module: helm_repository
short_description: Add and remove Helm repository
version_added: "0.11.0"
author:
- Lucas Boisserie (@LucasBoisserie)
@@ -24,7 +23,7 @@ requirements:
- "yaml (https://pypi.org/project/PyYAML/)"
description:
- Manage Helm repositories
- Manage Helm repositories.
options:
binary_path:
@@ -60,21 +59,21 @@ options:
repo_state:
choices: ['present', 'absent']
description:
- Desirated state of repositoriy.
- Desirated state of repository.
required: false
default: present
aliases: [ state ]
type: str
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Add default repository
helm_repository:
community.kubernetes.helm_repository:
name: stable
repo_url: https://kubernetes-charts.storage.googleapis.com
'''
RETURN = ''' # '''
RETURN = r''' # '''
import traceback

View File

@@ -9,11 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s
@@ -28,7 +24,7 @@ description:
- Pass the object definition from a source file or inline. See examples for reading
files and using Jinja templates or vault-encrypted files.
- Access to the full range of K8s APIs.
- Use the M(k8s_info) module to obtain a list of items about an object of type C(kind)
- Use the M(community.kubernetes.k8s_info) module to obtain a list of items about an object of type C(kind)
- Authenticate using either a config file, certificates, password or token.
- Supports check mode.
@@ -115,7 +111,7 @@ options:
validate:
description:
- how (if at all) to validate the resource definition against the kubernetes schema.
Requires the kubernetes-validate python module
Requires the kubernetes-validate python module and openshift >= 0.8.0
suboptions:
fail_on_error:
description: whether to fail on validation errors.
@@ -136,12 +132,14 @@ options:
- The full definition of an object is needed to generate the hash - this means that deleting an object created with append_hash
will only work if the same object is passed with state=absent (alternatively, just use state=absent with the name including
the generated hash and append_hash=no)
- Requires openshift >= 0.7.2
type: bool
apply:
description:
- C(apply) compares the desired resource definition with the previously supplied resource definition,
ignoring properties that are automatically generated
- C(apply) works better with Services than 'force=yes'
- Requires openshift >= 0.9.2
- mutually exclusive with C(merge_type)
type: bool
@@ -151,16 +149,16 @@ requirements:
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Create a k8s namespace
k8s:
community.kubernetes.k8s:
name: testing
api_version: v1
kind: Namespace
state: present
- name: Create a Service object from an inline definition
k8s:
community.kubernetes.k8s:
state: present
definition:
apiVersion: v1
@@ -182,7 +180,7 @@ EXAMPLES = '''
port: 8000
- name: Remove an existing Service object
k8s:
community.kubernetes.k8s:
state: absent
api_version: v1
kind: Service
@@ -192,31 +190,31 @@ EXAMPLES = '''
# Passing the object definition from a file
- name: Create a Deployment by reading the definition from a local file
k8s:
community.kubernetes.k8s:
state: present
src: /testing/deployment.yml
- name: >-
Read definition file from the Ansible controller file system.
If the definition file has been encrypted with Ansible Vault it will automatically be decrypted.
k8s:
community.kubernetes.k8s:
state: present
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
- name: Read definition file from the Ansible controller file system after Jinja templating
k8s:
community.kubernetes.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
- name: fail on validation errors
k8s:
community.kubernetes.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
validate:
fail_on_error: yes
- name: warn on validation errors, check for unexpected properties
k8s:
community.kubernetes.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
validate:
@@ -224,7 +222,7 @@ EXAMPLES = '''
strict: yes
'''
RETURN = '''
RETURN = r'''
result:
description:
- The created, patched, or otherwise present object. Will be empty in the case of a deletion.

View File

@@ -9,11 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_auth
@@ -79,7 +75,7 @@ requirements:
- requests-oauthlib
'''
EXAMPLES = '''
EXAMPLES = r'''
- hosts: localhost
module_defaults:
group/k8s:
@@ -92,7 +88,7 @@ EXAMPLES = '''
- include_vars: k8s_passwords.yml
- name: Log in (obtain access token)
k8s_auth:
community.kubernetes.k8s_auth:
username: admin
password: "{{ k8s_admin_password }}"
register: k8s_auth_results
@@ -100,7 +96,7 @@ EXAMPLES = '''
# Previous task provides the token/api_key, while all other parameters
# are taken from module_defaults
- name: Get a list of all pods from any namespace
k8s_info:
community.kubernetes.k8s_info:
api_key: "{{ k8s_auth_results.k8s_auth.api_key }}"
kind: Pod
register: pod_list
@@ -108,7 +104,7 @@ EXAMPLES = '''
always:
- name: If login succeeded, try to log out (revoke access token)
when: k8s_auth_results.k8s_auth.api_key is defined
k8s_auth:
community.kubernetes.k8s_auth:
state: absent
api_key: "{{ k8s_auth_results.k8s_auth.api_key }}"
'''
@@ -116,7 +112,7 @@ EXAMPLES = '''
# Returned value names need to match k8s modules parameter names, to make it
# easy to pass returned values of k8s_auth to other k8s modules.
# Discussion: https://github.com/ansible/ansible/pull/50807#discussion_r248827899
RETURN = '''
RETURN = r'''
k8s_auth:
description: Kubernetes authentication facts.
returned: success
@@ -255,7 +251,7 @@ class KubernetesAuthModule(AnsibleModule):
self.openshift_auth_endpoint = oauth_info['authorization_endpoint']
self.openshift_token_endpoint = oauth_info['token_endpoint']
except Exception as e:
except Exception:
self.fail_json(msg="Something went wrong discovering OpenShift OAuth details.",
exception=traceback.format_exc())
@@ -315,7 +311,7 @@ class KubernetesAuthModule(AnsibleModule):
"kind": "DeleteOptions"
}
ret = requests.delete(url, headers=headers, json=json, verify=self.con_verify_ca)
requests.delete(url, headers=headers, json=json, verify=self.con_verify_ca)
# Ignore errors, the token will time out eventually anyway
def fail(self, msg=None):

View File

@@ -9,16 +9,14 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_exec
short_description: Execute command in Pod
version_added: "0.10.0"
author: "Tristan de Cacqueray (@tristanC)"
description:
@@ -32,10 +30,13 @@ requirements:
- "openshift == 0.4.3"
- "PyYAML >= 3.11"
notes:
- Return code C(return_code) for the command executed is added in output in version 1.0.0.
options:
proxy:
description:
- The URL of an HTTP proxy to use for the connection. Can also be specified via K8S_AUTH_PROXY environment variable.
- The URL of an HTTP proxy to use for the connection.
- Can also be specified via I(K8S_AUTH_PROXY) environment variable.
- Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).
type: str
namespace:
@@ -50,7 +51,8 @@ options:
required: yes
container:
description:
- The name of the container in the pod to connect to. Defaults to only container if there is only one container in the pod.
- The name of the container in the pod to connect to.
- Defaults to only container if there is only one container in the pod.
type: str
required: no
command:
@@ -60,15 +62,28 @@ options:
required: yes
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Execute a command
k8s_exec:
community.kubernetes.k8s_exec:
namespace: myproject
pod: zuul-scheduler
command: zuul-scheduler full-reconfigure
- name: Check RC status of command executed
community.kubernetes.k8s_exec:
namespace: myproject
pod: busybox-test
command: cmd_with_non_zero_exit_code
register: command_status
ignore_errors: True
- name: Check last command status
debug:
msg: "cmd failed"
when: command_status.return_code != 0
'''
RETURN = '''
RETURN = r'''
result:
description:
- The command object
@@ -87,12 +102,25 @@ result:
stderr_lines:
description: The command stderr
type: str
return_code:
description: The command status code
type: int
'''
import copy
import shlex
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC
try:
import yaml
except ImportError:
# ImportError are managed by the common module already.
pass
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, AUTH_ARG_SPEC
)
try:
from kubernetes.client.apis import core_v1_api
@@ -102,7 +130,18 @@ except ImportError:
pass
class KubernetesExecCommand(KubernetesAnsibleModule):
class KubernetesExecCommand(K8sAnsibleMixin):
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 argspec(self):
spec = copy.deepcopy(AUTH_ARG_SPEC)
@@ -112,36 +151,54 @@ class KubernetesExecCommand(KubernetesAnsibleModule):
spec['command'] = dict(type='str', required=True)
return spec
def execute_module(self):
# Load kubernetes.client.Configuration
self.get_api_client()
api = core_v1_api.CoreV1Api()
# hack because passing the container as None breaks things
optional_kwargs = {}
if self.params.get('container'):
optional_kwargs['container'] = self.params['container']
try:
resp = stream(
api.connect_get_namespaced_pod_exec,
self.params["pod"],
self.params["namespace"],
command=shlex.split(self.params["command"]),
stdout=True,
stderr=True,
stdin=False,
tty=False,
_preload_content=False, **optional_kwargs)
except Exception as e:
self.module.fail_json(msg="Failed to execute on pod %s"
" due to : %s" % (self.params.get('pod'), to_native(e)))
stdout, stderr, rc = [], [], 0
while resp.is_open():
resp.update(timeout=1)
if resp.peek_stdout():
stdout.append(resp.read_stdout())
if resp.peek_stderr():
stderr.append(resp.read_stderr())
err = resp.read_channel(3)
err = yaml.safe_load(err)
if err['status'] == 'Success':
rc = 0
else:
rc = int(err['details']['causes'][0]['message'])
self.module.exit_json(
# Some command might change environment, but ultimately failing at end
changed=True,
stdout="".join(stdout),
stderr="".join(stderr),
return_code=rc
)
def main():
module = KubernetesExecCommand()
# Load kubernetes.client.Configuration
module.get_api_client()
api = core_v1_api.CoreV1Api()
# hack because passing the container as None breaks things
optional_kwargs = {}
if module.params.get('container'):
optional_kwargs['container'] = module.params['container']
resp = stream(
api.connect_get_namespaced_pod_exec,
module.params["pod"],
module.params["namespace"],
command=shlex.split(module.params["command"]),
stdout=True,
stderr=True,
stdin=False,
tty=False,
_preload_content=False, **optional_kwargs)
stdout, stderr = [], []
while resp.is_open():
resp.update(timeout=1)
if resp.peek_stdout():
stdout.append(resp.read_stdout())
if resp.peek_stderr():
stderr.append(resp.read_stderr())
module.exit_json(
changed=True, stdout="".join(stdout), stderr="".join(stderr))
KubernetesExecCommand().execute_module()
if __name__ == '__main__':

View File

@@ -9,11 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_info
short_description: Describe Kubernetes (K8s) objects
@@ -29,31 +25,15 @@ description:
- This module was called C(k8s_facts) before Ansible 2.9. The usage did not change.
options:
api_version:
description:
- Use to specify the API version. in conjunction with I(kind), I(name), and I(namespace) to identify a
specific object.
default: v1
aliases:
- api
- version
type: str
kind:
description:
- Use to specify an object model. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a
specific object.
required: yes
type: str
name:
description:
- Use to specify an object name. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a
specific object.
type: str
namespace:
description:
- Use to specify an object namespace. Use in conjunction with I(api_version), I(kind), and I(name)
to identify a specific object.
- Use to specify an object model.
- Use to create, delete, or discover an object without providing a full resource definition.
- Use in conjunction with I(api_version), I(name), and I(namespace) to identify a specific object.
- If I(resource definition) is provided, the I(kind) value from the I(resource_definition)
will override this option.
type: str
required: True
label_selectors:
description: List of label selectors to use to filter results
type: list
@@ -65,6 +45,7 @@ options:
extends_documentation_fragment:
- community.kubernetes.k8s_auth_options
- community.kubernetes.k8s_name_options
requirements:
- "python >= 2.7"
@@ -72,9 +53,9 @@ requirements:
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Get an existing Service object
k8s_info:
community.kubernetes.k8s_info:
api_version: v1
kind: Service
name: web
@@ -82,32 +63,45 @@ EXAMPLES = '''
register: web_service
- name: Get a list of all service objects
k8s_info:
community.kubernetes.k8s_info:
api_version: v1
kind: Service
namespace: testing
register: service_list
- name: Get a list of all pods from any namespace
k8s_info:
community.kubernetes.k8s_info:
kind: Pod
register: pod_list
- name: Search for all Pods labelled app=web
k8s_info:
community.kubernetes.k8s_info:
kind: Pod
label_selectors:
- app = web
- tier in (dev, test)
- name: Using vars while using label_selectors
community.kubernetes.k8s_info:
kind: Pod
label_selectors:
- "app = {{ app_label_web }}"
vars:
app_label_web: web
- name: Search for all running pods
k8s_info:
community.kubernetes.k8s_info:
kind: Pod
field_selectors:
- status.phase=Running
- name: List custom objects created using CRD
community.kubernetes.k8s_info:
kind: MyCustomObject
api_version: "stable.example.com/v1"
'''
RETURN = '''
RETURN = r'''
resources:
description:
- The object(s) that exists
@@ -136,19 +130,25 @@ resources:
type: dict
'''
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule, AUTH_ARG_SPEC
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, AUTH_ARG_SPEC)
class KubernetesInfoModule(KubernetesAnsibleModule):
class KubernetesInfoModule(K8sAnsibleMixin):
def __init__(self, *args, **kwargs):
KubernetesAnsibleModule.__init__(self, *args,
supports_check_mode=True,
**kwargs)
if self._name == 'k8s_facts':
self.deprecate("The 'k8s_facts' module has been renamed to 'k8s_info'", version='2.13')
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
self.exit_json = self.module.exit_json
super(KubernetesInfoModule, self).__init__()
def execute_module(self):
self.client = self.get_api_client()

View File

@@ -9,15 +9,13 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_log
short_description: Fetch logs from Kubernetes resources
version_added: "0.10.0"
author:
- "Fabian von Feilitzsch (@fabianvf)"
@@ -28,40 +26,25 @@ description:
- Analogous to `kubectl logs` or `oc logs`
extends_documentation_fragment:
- community.kubernetes.k8s_auth_options
- community.kubernetes.k8s_name_options
options:
api_version:
description:
- Use to specify the API version. in conjunction with I(kind), I(name), and I(namespace) to identify a
specific object.
- If using I(label_selector), cannot be overridden
default: v1
aliases:
- api
- version
type: str
kind:
description:
- Use to specify an object model. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a
specific object.
- If using I(label_selector), cannot be overridden
required: no
- Use to specify an object model.
- Use in conjunction with I(api_version), I(name), and I(namespace) to identify a specific object.
- If using I(label_selectors), cannot be overridden.
type: str
default: Pod
type: str
namespace:
description:
- Use to specify an object namespace. Use in conjunction with I(api_version), I(kind), and I(name)
to identify a specfic object.
type: str
name:
description:
- Use to specify an object name. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a
specific object.
- Only one of I(name) or I(label_selector) may be provided
- Use to specify an object name.
- Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a specific object.
- Only one of I(name) or I(label_selectors) may be provided.
type: str
label_selectors:
description:
- List of label selectors to use to filter results
- Only one of I(name) or I(label_selector) may be provided
- Only one of I(name) or I(label_selectors) may be provided.
type: list
elements: str
container:
@@ -78,16 +61,16 @@ requirements:
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Get a log from a Pod
k8s_log:
community.kubernetes.k8s_log:
name: example-1
namespace: testing
register: log
# This will get the log from the first Pod found matching the selector
- name: Log a Pod matching a label selector
k8s_log:
community.kubernetes.k8s_log:
namespace: testing
label_selectors:
- app=example
@@ -95,7 +78,7 @@ EXAMPLES = '''
# This will get the log from a single Pod managed by this Deployment
- name: Get a log from a Deployment
k8s_log:
community.kubernetes.k8s_log:
api_version: apps/v1
kind: Deployment
namespace: testing
@@ -104,7 +87,7 @@ EXAMPLES = '''
# This will get the log from a single Pod managed by this DeploymentConfig
- name: Get a log from a DeploymentConfig
k8s_log:
community.kubernetes.k8s_log:
api_version: apps.openshift.io/v1
kind: DeploymentConfig
namespace: testing
@@ -112,7 +95,7 @@ EXAMPLES = '''
register: log
'''
RETURN = '''
RETURN = r'''
log:
type: str
description:
@@ -128,28 +111,34 @@ log_lines:
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import PY2
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.common import (
K8sAnsibleMixin, AUTH_ARG_SPEC, NAME_ARG_SPEC)
class KubernetesLogModule(KubernetesAnsibleModule):
class KubernetesLogModule(K8sAnsibleMixin):
def __init__(self, *args, **kwargs):
KubernetesAnsibleModule.__init__(self, *args,
supports_check_mode=True,
**kwargs)
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
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(default='Pod'),
api_version=dict(default='v1', aliases=['api', 'version']),
name=dict(),
namespace=dict(),
kind=dict(type='str', default='Pod'),
container=dict(),
label_selectors=dict(type='list', elements='str', default=[]),
)
@@ -158,6 +147,7 @@ class KubernetesLogModule(KubernetesAnsibleModule):
def execute_module(self):
name = self.params.get('name')
namespace = self.params.get('namespace')
label_selector = ','.join(self.params.get('label_selectors', {}))
if name and label_selector:
self.fail(msg='Only one of name or label_selectors can be provided')
@@ -167,16 +157,16 @@ class KubernetesLogModule(KubernetesAnsibleModule):
v1_pods = self.find_resource('Pod', 'v1', fail=True)
if 'log' not in resource.subresources:
if not self.params.get('name'):
if not name:
self.fail(msg='name must be provided for resources that do not support the log subresource')
instance = resource.get(name=self.params['name'], namespace=self.params.get('namespace'))
instance = resource.get(name=name, namespace=namespace)
label_selector = ','.join(self.extract_selectors(instance))
resource = v1_pods
if label_selector:
instances = v1_pods.get(namespace=self.params['namespace'], label_selector=label_selector)
instances = v1_pods.get(namespace=namespace, label_selector=label_selector)
if not instances.items:
self.fail(msg='No pods in namespace {0} matched selector {1}'.format(self.params['namespace'], label_selector))
self.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
name = instances.items[0].metadata.name
resource = v1_pods
@@ -187,7 +177,7 @@ class KubernetesLogModule(KubernetesAnsibleModule):
log = serialize_log(resource.log.get(
name=name,
namespace=self.params.get('namespace'),
namespace=namespace,
serialize=False,
**kwargs
))

View File

@@ -9,11 +9,8 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_scale
@@ -39,9 +36,9 @@ requirements:
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Scale deployment up, and extend timeout
k8s_scale:
community.kubernetes.k8s_scale:
api_version: v1
kind: Deployment
name: elastic
@@ -50,7 +47,7 @@ EXAMPLES = '''
wait_timeout: 60
- name: Scale deployment down when current replicas match
k8s_scale:
community.kubernetes.k8s_scale:
api_version: v1
kind: Deployment
name: elastic
@@ -59,7 +56,7 @@ EXAMPLES = '''
replicas: 2
- name: Increase job parallelism
k8s_scale:
community.kubernetes.k8s_scale:
api_version: batch/v1
kind: job
name: pi-with-timeout
@@ -69,25 +66,25 @@ EXAMPLES = '''
# Match object using local file or inline definition
- name: Scale deployment based on a file from the local filesystem
k8s_scale:
community.kubernetes.k8s_scale:
src: /myproject/elastic_deployment.yml
replicas: 3
wait: no
- name: Scale deployment based on a template output
k8s_scale:
community.kubernetes.k8s_scale:
resource_definition: "{{ lookup('template', '/myproject/elastic_deployment.yml') | from_yaml }}"
replicas: 3
wait: no
- name: Scale deployment based on a file from the Ansible controller filesystem
k8s_scale:
community.kubernetes.k8s_scale:
resource_definition: "{{ lookup('file', '/myproject/elastic_deployment.yml') | from_yaml }}"
replicas: 3
wait: no
'''
RETURN = '''
RETURN = r'''
result:
description:
- If a change was made, will return the patched object, otherwise returns the existing object.
@@ -114,6 +111,11 @@ result:
description: Current status details for the object.
returned: success
type: complex
duration:
description: elapsed time of task in seconds
returned: when C(wait) is true
type: int
sample: 48
'''
from ansible_collections.community.kubernetes.plugins.module_utils.scale import KubernetesAnsibleScaleModule

View File

@@ -9,11 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: k8s_service
@@ -26,43 +22,10 @@ description:
extends_documentation_fragment:
- community.kubernetes.k8s_auth_options
- community.kubernetes.k8s_resource_options
- community.kubernetes.k8s_state_options
options:
resource_definition:
description:
- A partial YAML definition of the Service object being created/updated. Here you can define Kubernetes
Service Resource parameters not covered by this module's parameters.
- "NOTE: I(resource_definition) has lower priority than module parameters. If you try to define e.g.
I(metadata.namespace) here, that value will be ignored and I(metadata) used instead."
aliases:
- definition
- inline
type: dict
src:
description:
- "Provide a path to a file containing a valid YAML definition of an object dated. Mutually
exclusive with I(resource_definition). NOTE: I(kind), I(api_version), I(resource_name), and I(namespace)
will be overwritten by corresponding values found in the configuration read in from the I(src) file."
- Reads from the local file system. To read from the Ansible controller's file system, use the file lookup
plugin or template lookup plugin, combined with the from_yaml filter, and pass the result to
I(resource_definition). See Examples below.
type: path
state:
description:
- Determines if an object should be created, patched, or deleted. When set to C(present), an object will be
created, if it does not already exist. If set to C(absent), an existing object will be deleted. If set to
C(present), an existing object will be patched, if its attributes differ from those specified using
module options and I(resource_definition).
default: present
choices:
- present
- absent
type: str
force:
description:
- If set to C(True), and I(state) is C(present), an existing object will be replaced.
default: false
type: bool
merge_type:
description:
- Whether to override the default patch merge approach with a specific type. By default, the strategic
@@ -125,9 +88,9 @@ requirements:
- openshift >= 0.6.2
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Expose https port with ClusterIP
k8s_service:
community.kubernetes.k8s_service:
state: present
name: test-https
namespace: default
@@ -138,7 +101,7 @@ EXAMPLES = '''
key: special
- name: Expose https port with ClusterIP using spec
k8s_service:
community.kubernetes.k8s_service:
state: present
name: test-https
namespace: default
@@ -151,7 +114,7 @@ EXAMPLES = '''
key: special
'''
RETURN = '''
RETURN = r'''
result:
description:
- The created, patched, or otherwise present Service object. Will be empty in the case of a deletion.
@@ -185,7 +148,7 @@ import traceback
from collections import defaultdict
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule
@@ -194,25 +157,10 @@ SERVICE_ARG_SPEC = {
'type': 'bool',
'default': False,
},
'state': {
'default': 'present',
'choices': ['present', 'absent'],
},
'force': {
'type': 'bool',
'default': False,
},
'resource_definition': {
'type': 'dict',
'aliases': ['definition', 'inline']
},
'name': {'required': True},
'namespace': {'required': True},
'merge_type': {'type': 'list', 'elements': 'str', 'choices': ['json', 'merge', 'strategic-merge']},
'selector': {'type': 'dict'},
'src': {
'type': 'path',
},
'type': {
'type': 'str',
'choices': [
@@ -244,6 +192,8 @@ class KubernetesService(KubernetesRawModule):
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