Issue #1, Issue #2: Move more content into collection, fix tests.

This commit is contained in:
Jeff Geerling
2020-01-30 13:51:43 -06:00
parent 2e807f946a
commit d94dd72ae6
53 changed files with 15728 additions and 12 deletions

View File

@@ -0,0 +1,356 @@
# Based on the docker connection plugin
#
# Connection plugin for configuring kubernetes containers with kubectl
# (c) 2017, XuXinkun <xuxinkun@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
author:
- xuxinkun
connection: kubectl
short_description: Execute tasks in pods running on Kubernetes.
description:
- Use the kubectl exec command to run tasks in, or put/fetch files to, pods running on the Kubernetes
container platform.
version_added: "2.5"
requirements:
- kubectl (go binary)
options:
kubectl_pod:
description:
- Pod name. Required when the host name does not match pod name.
default: ''
vars:
- name: ansible_kubectl_pod
env:
- name: K8S_AUTH_POD
kubectl_container:
description:
- Container name. Required when a pod contains more than one container.
default: ''
vars:
- name: ansible_kubectl_container
env:
- name: K8S_AUTH_CONTAINER
kubectl_namespace:
description:
- The namespace of the pod
default: ''
vars:
- name: ansible_kubectl_namespace
env:
- name: K8S_AUTH_NAMESPACE
kubectl_extra_args:
description:
- Extra arguments to pass to the kubectl command line.
default: ''
vars:
- name: ansible_kubectl_extra_args
env:
- name: K8S_AUTH_EXTRA_ARGS
kubectl_kubeconfig:
description:
- Path to a kubectl config file. Defaults to I(~/.kube/config)
default: ''
vars:
- name: ansible_kubectl_kubeconfig
- name: ansible_kubectl_config
env:
- name: K8S_AUTH_KUBECONFIG
kubectl_context:
description:
- The name of a context found in the K8s config file.
default: ''
vars:
- name: ansible_kubectl_context
env:
- name: k8S_AUTH_CONTEXT
kubectl_host:
description:
- URL for accessing the API.
default: ''
vars:
- name: ansible_kubectl_host
- name: ansible_kubectl_server
env:
- name: K8S_AUTH_HOST
- name: K8S_AUTH_SERVER
kubectl_username:
description:
- Provide a username for authenticating with the API.
default: ''
vars:
- name: ansible_kubectl_username
- name: ansible_kubectl_user
env:
- name: K8S_AUTH_USERNAME
kubectl_password:
description:
- Provide a password for authenticating with the API.
default: ''
vars:
- name: ansible_kubectl_password
env:
- name: K8S_AUTH_PASSWORD
kubectl_token:
description:
- API authentication bearer token.
vars:
- name: ansible_kubectl_token
- name: ansible_kubectl_api_key
env:
- name: K8S_AUTH_TOKEN
- name: K8S_AUTH_API_KEY
client_cert:
description:
- Path to a certificate used to authenticate with the API.
default: ''
vars:
- name: ansible_kubectl_cert_file
- name: ansible_kubectl_client_cert
env:
- name: K8S_AUTH_CERT_FILE
aliases: [ kubectl_cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API.
default: ''
vars:
- name: ansible_kubectl_key_file
- name: ansible_kubectl_client_key
env:
- name: K8S_AUTH_KEY_FILE
aliases: [ kubectl_key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API.
default: ''
vars:
- name: ansible_kubectl_ssl_ca_cert
- name: ansible_kubectl_ca_cert
env:
- name: K8S_AUTH_SSL_CA_CERT
aliases: [ kubectl_ssl_ca_cert ]
validate_certs:
description:
- Whether or not to verify the API server's SSL certificate. Defaults to I(true).
default: ''
vars:
- name: ansible_kubectl_verify_ssl
- name: ansible_kubectl_validate_certs
env:
- name: K8S_AUTH_VERIFY_SSL
aliases: [ kubectl_verify_ssl ]
"""
import distutils.spawn
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
from ansible.module_utils._text import to_bytes
from ansible.plugins.connection import ConnectionBase, BUFSIZE
from ansible.utils.display import Display
display = Display()
CONNECTION_TRANSPORT = 'kubectl'
CONNECTION_OPTIONS = {
'kubectl_container': '-c',
'kubectl_namespace': '-n',
'kubectl_kubeconfig': '--kubeconfig',
'kubectl_context': '--context',
'kubectl_host': '--server',
'kubectl_username': '--username',
'kubectl_password': '--password',
'client_cert': '--client-certificate',
'client_key': '--client-key',
'ca_cert': '--certificate-authority',
'validate_certs': '--insecure-skip-tls-verify',
'kubectl_token': '--token'
}
class Connection(ConnectionBase):
''' Local kubectl based connections '''
transport = CONNECTION_TRANSPORT
connection_options = CONNECTION_OPTIONS
documentation = DOCUMENTATION
has_pipelining = True
transport_cmd = None
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
# Note: kubectl runs commands as the user that started the container.
# It is impossible to set the remote user for a kubectl connection.
cmd_arg = '{0}_command'.format(self.transport)
if cmd_arg in kwargs:
self.transport_cmd = kwargs[cmd_arg]
else:
self.transport_cmd = distutils.spawn.find_executable(self.transport)
if not self.transport_cmd:
raise AnsibleError("{0} command not found in PATH".format(self.transport))
def _build_exec_cmd(self, cmd):
""" Build the local kubectl exec command to run cmd on remote_host
"""
local_cmd = [self.transport_cmd]
# Build command options based on doc string
doc_yaml = AnsibleLoader(self.documentation).get_single_data()
for key in doc_yaml.get('options'):
if key.endswith('verify_ssl') and self.get_option(key) != '':
# 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()))
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)]
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(' ')
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]
# 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)]
local_cmd += ['--'] + cmd
return local_cmd
def _connect(self, port=None):
""" Connect to the container. Nothing to do """
super(Connection, self)._connect()
if not self._connected:
display.vvv(u"ESTABLISH {0} CONNECTION".format(self.transport), host=self._play_context.remote_addr)
self._connected = True
def exec_command(self, cmd, in_data=None, sudoable=False):
""" 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])
display.vvv("EXEC %s" % (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)
stdout, stderr = p.communicate(in_data)
return (p.returncode, stdout, stderr)
def _prefix_login_path(self, remote_path):
''' Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it's a problem
'''
if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" Transfer a file from local to the container """
super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
out_path = self._prefix_login_path(out_path)
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
raise AnsibleFileNotFound(
"file or module does not exist: %s" % in_path)
out_path = shlex_quote(out_path)
# kubectl doesn't have native support for copying files into
# running containers, so we use kubectl exec to implement this
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
if not os.fstat(in_file.fileno()).st_size:
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 = [to_bytes(i, errors='surrogate_or_strict') for i in args]
try:
p = subprocess.Popen(args, stdin=in_file,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError:
raise AnsibleError("kubectl connection requires dd command in the container to put files")
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
def fetch_file(self, in_path, out_path):
""" Fetch a file from container to local. """
super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
in_path = self._prefix_login_path(in_path)
out_dir = os.path.dirname(out_path)
# 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 = [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:
try:
p = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=out_file, stderr=subprocess.PIPE)
except OSError:
raise AnsibleError(
"{0} connection requires dd command in the container to fetch files".format(self.transport)
)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError("failed to fetch file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
if actual_out_path != out_path:
os.rename(to_bytes(actual_out_path, errors='strict'), to_bytes(out_path, errors='strict'))
def close(self):
""" Terminate the connection. Nothing to do for kubectl"""
super(Connection, self).close()
self._connected = False

40
plugins/filter/k8s.py Normal file
View File

@@ -0,0 +1,40 @@
# Copyright (c) 2017 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'
}
try:
from openshift.helper.hashes import generate_hash
HAS_GENERATE_HASH = True
except ImportError:
HAS_GENERATE_HASH = False
from ansible.errors import AnsibleFilterError
def k8s_config_resource_name(resource):
if not HAS_GENERATE_HASH:
raise AnsibleFilterError("k8s_config_resource_name requires openshift>=0.7.2")
try:
return resource['metadata']['name'] + '-' + generate_hash(resource)
except KeyError:
raise AnsibleFilterError("resource must have a metadata.name key to generate a resource name")
# ---- Ansible filters ----
class FilterModule(object):
def filters(self):
return {
'k8s_config_resource_name': k8s_config_resource_name
}

360
plugins/inventory/k8s.py Normal file
View File

@@ -0,0 +1,360 @@
# Copyright (c) 2018 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 = '''
name: k8s
plugin_type: inventory
author:
- Chris Houseknecht <@chouseknecht>
- Fabian von Feilitzsch <@fabianvf>
short_description: Kubernetes (K8s) inventory source
description:
- Fetch containers and services for one or more clusters
- Groups by cluster name, namespace, namespace_services, namespace_pods, and labels
- Uses k8s.(yml|yaml) YAML configuration file to set parameter values.
options:
plugin:
description: token that ensures this is a source file for the 'k8s' plugin.
required: True
choices: ['k8s']
connections:
description:
- 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.
requirements:
- "python >= 2.7"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
# File must be named k8s.yaml or k8s.yml
# Authenticate with token, and return all pods and services for all namespaces
plugin: k8s
connections:
- host: https://192.168.64.4:8443
token: xxxxxxxxxxxxxxxx
validate_certs: false
# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
plugin: k8s
connections:
- namespaces:
- testing
# Use a custom config file, and a specific context.
plugin: k8s
connections:
- kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer'
'''
import json
from ansible.errors import AnsibleError
from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
try:
from openshift.dynamic.exceptions import DynamicApiError
except ImportError:
pass
def format_dynamic_api_exc(exc):
if exc.body:
if exc.headers and exc.headers.get('Content-Type') == 'application/json':
message = json.loads(exc.body).get('message')
if message:
return message
return exc.body
else:
return '%s Reason: %s' % (exc.status, exc.reason)
class K8sInventoryException(Exception):
pass
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin):
NAME = 'k8s'
transport = 'kubectl'
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
cache_key = self._get_cache_prefix(path)
config_data = self._read_config_data(path)
self.setup(config_data, cache, cache_key)
def setup(self, config_data, cache, cache_key):
connections = config_data.get('connections')
if not HAS_K8S_MODULE_HELPER:
raise K8sInventoryException(
"This module requires the OpenShift Python client. Try `pip install openshift`. Detail: {0}".format(k8s_import_exception)
)
source_data = None
if cache and cache_key in self._cache:
try:
source_data = self._cache[cache_key]
except KeyError:
pass
if not source_data:
self.fetch_objects(connections)
def fetch_objects(self, connections):
if connections:
if not isinstance(connections, list):
raise K8sInventoryException("Expecting connections to be a list.")
for connection in connections:
if not isinstance(connection, dict):
raise K8sInventoryException("Expecting connection to be a dictionary.")
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connection['namespaces']
else:
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
else:
client = self.get_api_client()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
@staticmethod
def get_default_host_name(host):
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
def get_available_namespaces(self, client):
v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
try:
obj = v1_namespace.get()
except DynamicApiError as exc:
self.display.debug(exc)
raise K8sInventoryException('Error fetching Namespace list: %s' % format_dynamic_api_exc(exc))
return [namespace.metadata.name for namespace in obj.items]
def get_pods_for_namespace(self, client, name, namespace):
v1_pod = client.resources.get(api_version='v1', kind='Pod')
try:
obj = v1_pod.get(namespace=namespace)
except DynamicApiError as exc:
self.display.debug(exc)
raise K8sInventoryException('Error fetching Pod list: %s' % format_dynamic_api_exc(exc))
namespace_group = 'namespace_{0}'.format(namespace)
namespace_pods_group = '{0}_pods'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_pods_group)
self.inventory.add_child(namespace_group, namespace_pods_group)
for pod in obj.items:
pod_name = pod.metadata.name
pod_groups = []
pod_annotations = {} if not pod.metadata.annotations else dict(pod.metadata.annotations)
if pod.metadata.labels:
# create a group for each label_value
for key, value in pod.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
if group_name not in pod_groups:
pod_groups.append(group_name)
self.inventory.add_group(group_name)
pod_labels = dict(pod.metadata.labels)
else:
pod_labels = {}
if not pod.status.containerStatuses:
continue
for container in pod.status.containerStatuses:
# add each pod_container to the namespace group, and to each label_value group
container_name = '{0}_{1}'.format(pod.metadata.name, container.name)
self.inventory.add_host(container_name)
self.inventory.add_child(namespace_pods_group, container_name)
if pod_groups:
for group in pod_groups:
self.inventory.add_child(group, container_name)
# Add hostvars
self.inventory.set_variable(container_name, 'object_type', 'pod')
self.inventory.set_variable(container_name, 'labels', pod_labels)
self.inventory.set_variable(container_name, 'annotations', pod_annotations)
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName)
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName)
self.inventory.set_variable(container_name, 'pod_name', pod.spec.name)
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP)
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase)
self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP)
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink)
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion)
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid)
self.inventory.set_variable(container_name, 'container_name', container.image)
self.inventory.set_variable(container_name, 'container_image', container.image)
if container.state.running:
self.inventory.set_variable(container_name, 'container_state', 'Running')
if container.state.terminated:
self.inventory.set_variable(container_name, 'container_state', 'Terminated')
if container.state.waiting:
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_{0}_pod'.format(self.transport),
pod_name)
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
container.name)
self.inventory.set_variable(container_name, 'ansible_{0}_namespace'.format(self.transport),
namespace)
def get_services_for_namespace(self, client, name, namespace):
v1_service = client.resources.get(api_version='v1', kind='Service')
try:
obj = v1_service.get(namespace=namespace)
except DynamicApiError as exc:
self.display.debug(exc)
raise K8sInventoryException('Error fetching Service list: %s' % format_dynamic_api_exc(exc))
namespace_group = 'namespace_{0}'.format(namespace)
namespace_services_group = '{0}_services'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_services_group)
self.inventory.add_child(namespace_group, namespace_services_group)
for service in obj.items:
service_name = service.metadata.name
service_labels = {} if not service.metadata.labels else dict(service.metadata.labels)
service_annotations = {} if not service.metadata.annotations else dict(service.metadata.annotations)
self.inventory.add_host(service_name)
if service.metadata.labels:
# create a group for each label_value
for key, value in service.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, service_name)
try:
self.inventory.add_child(namespace_services_group, service_name)
except AnsibleError as e:
raise
ports = [{'name': port.name,
'port': port.port,
'protocol': port.protocol,
'targetPort': port.targetPort,
'nodePort': port.nodePort} for port in service.spec.ports or []]
# add hostvars
self.inventory.set_variable(service_name, 'object_type', 'service')
self.inventory.set_variable(service_name, 'labels', service_labels)
self.inventory.set_variable(service_name, 'annotations', service_annotations)
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName)
self.inventory.set_variable(service_name, 'ports', ports)
self.inventory.set_variable(service_name, 'type', service.spec.type)
self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink)
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
self.inventory.set_variable(service_name, 'uid', service.metadata.uid)
if service.spec.externalTrafficPolicy:
self.inventory.set_variable(service_name, 'external_traffic_policy',
service.spec.externalTrafficPolicy)
if service.spec.externalIPs:
self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
if service.spec.externalName:
self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
if service.spec.healthCheckNodePort:
self.inventory.set_variable(service_name, 'health_check_node_port',
service.spec.healthCheckNodePort)
if service.spec.loadBalancerIP:
self.inventory.set_variable(service_name, 'load_balancer_ip',
service.spec.loadBalancerIP)
if service.spec.selector:
self.inventory.set_variable(service_name, 'selector', dict(service.spec.selector))
if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
load_balancer = [{'hostname': ingress.hostname,
'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
self.inventory.set_variable(service_name, 'load_balancer', load_balancer)

View File

@@ -0,0 +1,201 @@
# Copyright (c) 2018 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 = '''
name: openshift
plugin_type: inventory
author:
- Chris Houseknecht <@chouseknecht>
short_description: OpenShift inventory source
description:
- Fetch containers, services and routes for one or more clusters
- Groups by cluster name, namespace, namespace_services, namespace_pods, namespace_routes, and labels
- Uses openshift.(yml|yaml) YAML configuration file to set parameter values.
options:
plugin:
description: token that ensures this is a source file for the 'openshift' plugin.
required: True
choices: ['openshift']
connections:
description:
- 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.
requirements:
- "python >= 2.7"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
# File must be named openshift.yaml or openshift.yml
# Authenticate with token, and return all pods and services for all namespaces
plugin: 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
connections:
- namespaces:
- testing
# Use a custom config file, and a specific context.
plugin: openshift
connections:
- kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer'
'''
from ansible.plugins.inventory.k8s import K8sInventoryException, InventoryModule as K8sInventoryModule, format_dynamic_api_exc
try:
from openshift.dynamic.exceptions import DynamicApiError
except ImportError:
pass
class InventoryModule(K8sInventoryModule):
NAME = 'openshift'
transport = 'oc'
def fetch_objects(self, connections):
super(InventoryModule, self).fetch_objects(connections)
if connections:
if not isinstance(connections, list):
raise K8sInventoryException("Expecting connections to be a list.")
for connection in connections:
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connection['namespaces']
else:
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(client, name, namespace)
else:
client = self.get_api_client()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(client, name, namespace)
def get_routes_for_namespace(self, client, name, namespace):
v1_route = client.resources.get(api_version='v1', kind='Route')
try:
obj = v1_route.get(namespace=namespace)
except DynamicApiError as exc:
self.display.debug(exc)
raise K8sInventoryException('Error fetching Routes list: %s' % format_dynamic_api_exc(exc))
namespace_group = 'namespace_{0}'.format(namespace)
namespace_routes_group = '{0}_routes'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_routes_group)
self.inventory.add_child(namespace_group, namespace_routes_group)
for route in obj.items:
route_name = route.metadata.name
route_annotations = {} if not route.metadata.annotations else dict(route.metadata.annotations)
self.inventory.add_host(route_name)
if route.metadata.labels:
# create a group for each label_value
for key, value in route.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, route_name)
route_labels = dict(route.metadata.labels)
else:
route_labels = {}
self.inventory.add_child(namespace_routes_group, route_name)
# add hostvars
self.inventory.set_variable(route_name, 'labels', route_labels)
self.inventory.set_variable(route_name, 'annotations', route_annotations)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.clusterName)
self.inventory.set_variable(route_name, 'object_type', 'route')
self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion)
self.inventory.set_variable(route_name, 'uid', route.metadata.uid)
if route.spec.host:
self.inventory.set_variable(route_name, 'host', route.spec.host)
if route.spec.path:
self.inventory.set_variable(route_name, 'path', route.spec.path)
if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
self.inventory.set_variable(route_name, 'port', dict(route.spec.port))

299
plugins/lookup/k8s.py Normal file
View File

@@ -0,0 +1,299 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
lookup: k8s
version_added: "2.5"
short_description: Query the K8s API
description:
- Uses the OpenShift Python client to fetch a specific object by name, all matching objects within a
namespace, or all matching objects for all namespaces, as well as information about the cluster.
- Provides access the full range of K8s APIs.
- Enables authentication via config file, certificates, password or token.
options:
cluster_info:
description:
- Use to specify the type of cluster information you are attempting to retrieve. Will take priority
over all the other options.
api_version:
description:
- Use to specify the API version. If I(resource definition) is provided, the I(apiVersion) from the
I(resource_definition) will override this option.
default: v1
kind:
description:
- Use to specify an object model. If I(resource definition) is provided, the I(kind) from a
I(resource_definition) will override this option.
required: true
resource_name:
description:
- Fetch a specific object by name. If I(resource definition) is provided, the I(metadata.name) value
from the I(resource_definition) will override this option.
namespace:
description:
- Limit the objects returned to a specific namespace. If I(resource definition) is provided, the
I(metadata.namespace) value from the I(resource_definition) will override this option.
label_selector:
description:
- Additional labels to include in the query. Ignored when I(resource_name) is provided.
field_selector:
description:
- Specific fields on which to query. Ignored when I(resource_name) is provided.
resource_definition:
description:
- "Provide a YAML configuration for an object. NOTE: I(kind), I(api_version), I(resource_name),
and I(namespace) will be overwritten by corresponding values found in the provided I(resource_definition)."
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.
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.
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.
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 ]
requirements:
- "python >= 2.7"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
notes:
- "The OpenShift Python client wraps the K8s Python client, providing full access to
all of the APIS and models available on both platforms. For API version details and
additional information visit https://github.com/openshift/openshift-restclient-python"
"""
EXAMPLES = """
- name: Fetch a list of namespaces
set_fact:
projects: "{{ lookup('k8s', api_version='v1', kind='Namespace') }}"
- name: Fetch all deployments
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment') }}"
- name: Fetch all deployments in a namespace
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment', namespace='testing') }}"
- name: Fetch a specific deployment by name
set_fact:
deployments: "{{ lookup('k8s', kind='Deployment', namespace='testing', resource_name='elastic') }}"
- name: Fetch with label selector
set_fact:
service: "{{ lookup('k8s', kind='Service', label_selector='app=galaxy') }}"
# Use parameters from a YAML config
- name: Load config from the Ansible controller filesystem
set_fact:
config: "{{ lookup('file', 'service.yml') | from_yaml }}"
- 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) }}"
- name: Use a config from the local filesystem
set_fact:
service: "{{ lookup('k8s', src='service.yml') }}"
"""
RETURN = """
_list:
description:
- One ore more object definitions returned from the API.
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: complex
status:
description: Current status details for the object.
returned: success
type: complex
"""
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
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):
def __init__(self):
if not HAS_K8S_MODULE_HELPER:
raise Exception(
"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
self.api_version = None
self.label_selector = None
self.field_selector = None
self.include_uninitialized = None
self.resource_definition = None
self.helper = None
self.connection = {}
def fail(self, msg=None):
raise AnsibleError(msg)
def run(self, terms, variables=None, **kwargs):
self.params = kwargs
self.client = self.get_api_client()
cluster_info = kwargs.get('cluster_info')
if cluster_info == 'version':
return [self.client.version]
if cluster_info == 'api_groups':
return [self.client.resources.api_groups]
self.kind = kwargs.get('kind')
self.name = kwargs.get('resource_name')
self.namespace = kwargs.get('namespace')
self.api_version = kwargs.get('api_version', 'v1')
self.label_selector = kwargs.get('label_selector')
self.field_selector = kwargs.get('field_selector')
self.include_uninitialized = kwargs.get('include_uninitialized', False)
resource_definition = kwargs.get('resource_definition')
src = kwargs.get('src')
if src:
resource_definition = self.load_resource_definitions(src)[0]
if resource_definition:
self.kind = resource_definition.get('kind', self.kind)
self.api_version = resource_definition.get('apiVersion', self.api_version)
self.name = resource_definition.get('metadata', {}).get('name', self.name)
self.namespace = resource_definition.get('metadata', {}).get('namespace', self.namespace)
if not self.kind:
raise AnsibleError(
"Error: no Kind specified. Use the 'kind' parameter, or provide an object YAML configuration "
"using the 'resource_definition' parameter."
)
resource = self.find_resource(self.kind, self.api_version, fail=True)
try:
k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector)
except NotFoundError:
return []
if self.name:
return [k8s_obj.to_dict()]
return k8s_obj.to_dict().get('items')
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
return KubernetesLookup().run(terms, variables=variables, **kwargs)

View File

View File

@@ -0,0 +1,286 @@
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
import copy
import json
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
K8S_IMP_ERR = None
try:
import kubernetes
import openshift
from openshift.dynamic import DynamicClient
from openshift.dynamic.exceptions import ResourceNotFoundError, ResourceNotUniqueError
HAS_K8S_MODULE_HELPER = True
k8s_import_exception = None
except ImportError as e:
HAS_K8S_MODULE_HELPER = False
k8s_import_exception = e
K8S_IMP_ERR = traceback.format_exc()
YAML_IMP_ERR = None
try:
import yaml
HAS_YAML = True
except ImportError:
YAML_IMP_ERR = traceback.format_exc()
HAS_YAML = False
try:
import urllib3
urllib3.disable_warnings()
except ImportError:
pass
def list_dict_str(value):
if isinstance(value, list):
return value
elif isinstance(value, dict):
return value
elif isinstance(value, string_types):
return value
raise TypeError
ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
COMMON_ARG_SPEC = {
'state': {
'default': 'present',
'choices': ['present', 'absent'],
},
'force': {
'type': 'bool',
'default': False,
},
'resource_definition': {
'type': list_dict_str,
'aliases': ['definition', 'inline']
},
'src': {
'type': 'path',
},
'kind': {},
'name': {},
'namespace': {},
'api_version': {
'default': 'v1',
'aliases': ['api', 'version'],
},
}
AUTH_ARG_SPEC = {
'kubeconfig': {
'type': 'path',
},
'context': {},
'host': {},
'api_key': {
'no_log': True,
},
'username': {},
'password': {
'no_log': True,
},
'validate_certs': {
'type': 'bool',
'aliases': ['verify_ssl'],
},
'ca_cert': {
'type': 'path',
'aliases': ['ssl_ca_cert'],
},
'client_cert': {
'type': 'path',
'aliases': ['cert_file'],
},
'client_key': {
'type': 'path',
'aliases': ['key_file'],
},
'proxy': {},
}
# Map kubernetes-client parameters to ansible parameters
AUTH_ARG_MAP = {
'kubeconfig': 'kubeconfig',
'context': 'context',
'host': 'host',
'api_key': 'api_key',
'username': 'username',
'password': 'password',
'verify_ssl': 'validate_certs',
'ssl_ca_cert': 'ca_cert',
'cert_file': 'client_cert',
'key_file': 'client_key',
'proxy': 'proxy',
}
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 get_api_client(self, **auth_params):
auth_params = auth_params or getattr(self, 'params', {})
auth = {}
# If authorization variables aren't defined, look for them in environment variables
for true_name, arg_name in AUTH_ARG_MAP.items():
if auth_params.get(arg_name) is None:
env_value = os.getenv('K8S_AUTH_{0}'.format(arg_name.upper()), None) or os.getenv('K8S_AUTH_{0}'.format(true_name.upper()), None)
if env_value is not None:
if AUTH_ARG_SPEC[arg_name].get('type') == 'bool':
env_value = env_value.lower() not in ['0', 'false', 'no']
auth[true_name] = env_value
else:
auth[true_name] = auth_params[arg_name]
def auth_set(*names):
return all([auth.get(name) for name in names])
if auth_set('username', 'password', 'host') or auth_set('api_key', 'host'):
# We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
pass
elif auth_set('kubeconfig') or auth_set('context'):
kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'))
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'))
# Override any values in the default configuration with Ansible parameters
configuration = kubernetes.client.Configuration()
for key, value in iteritems(auth):
if key in AUTH_ARG_MAP.keys() and value is not None:
if key == 'api_key':
setattr(configuration, key, {'authorization': "Bearer {0}".format(value)})
else:
setattr(configuration, key, value)
kubernetes.client.Configuration.set_default(configuration)
return DynamicClient(kubernetes.client.ApiClient(configuration))
def find_resource(self, kind, api_version, fail=False):
for attribute in ['kind', 'name', 'singular_name']:
try:
return self.client.resources.get(**{'api_version': api_version, attribute: kind})
except (ResourceNotFoundError, ResourceNotUniqueError):
pass
try:
return self.client.resources.get(api_version=api_version, short_names=[kind])
except (ResourceNotFoundError, ResourceNotUniqueError):
if fail:
self.fail(msg='Failed to find exact match for {0}.{1} by [kind, name, singularName, shortNames]'.format(api_version, kind))
def kubernetes_facts(self, kind, api_version, name=None, namespace=None, label_selectors=None, field_selectors=None):
resource = self.find_resource(kind, api_version)
if not resource:
return dict(resources=[])
try:
result = resource.get(name=name,
namespace=namespace,
label_selector=','.join(label_selectors),
field_selector=','.join(field_selectors)).to_dict()
except openshift.dynamic.exceptions.NotFoundError:
return dict(resources=[])
if 'items' in result:
return dict(resources=result['items'])
else:
return dict(resources=[result])
def remove_aliases(self):
"""
The helper doesn't know what to do with aliased keys
"""
for k, v in iteritems(self.argspec):
if 'aliases' in v:
for alias in v['aliases']:
if alias in self.params:
self.params.pop(alias)
def load_resource_definitions(self, src):
""" Load the requested src path """
result = None
path = os.path.normpath(src)
if not os.path.exists(path):
self.fail(msg="Error accessing {0}. Does the file exist?".format(path))
try:
with open(path, 'r') as f:
result = list(yaml.safe_load_all(f))
except (IOError, yaml.YAMLError) as exc:
self.fail(msg="Error loading resource_definition: {0}".format(exc))
return result
@staticmethod
def diff_objects(existing, new):
result = dict()
diff = recursive_diff(existing, new)
if diff:
result['before'] = diff[0]
result['after'] = diff[1]
return not diff, result
class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
resource_definition = None
api_version = None
kind = None
def __init__(self, *args, **kwargs):
kwargs['argument_spec'] = self.argspec
AnsibleModule.__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 execute_module(self):
raise NotImplementedError()
def fail(self, msg=None):
self.fail_json(msg=msg)

514
plugins/module_utils/raw.py Normal file
View File

@@ -0,0 +1,514 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
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.common.dict_transformations import dict_merge
try:
import yaml
from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError, ForbiddenError, KubernetesValidateMissing
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:
from openshift.helper.hashes import generate_hash
HAS_K8S_CONFIG_HASH = True
except ImportError:
K8S_CONFIG_HASH_IMP_ERR = traceback.format_exc()
HAS_K8S_CONFIG_HASH = False
HAS_K8S_APPLY = None
try:
from openshift.dynamic.apply import apply_object
HAS_K8S_APPLY = True
except ImportError:
HAS_K8S_APPLY = False
class KubernetesRawModule(KubernetesAnsibleModule):
@property
def validate_spec(self):
return dict(
fail_on_error=dict(type='bool'),
version=dict(),
strict=dict(type='bool', default=True)
)
@property
def condition_spec(self):
return dict(
type=dict(),
status=dict(default=True, choices=[True, False, "Unknown"]),
reason=dict()
)
@property
def argspec(self):
argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
argument_spec['merge_type'] = dict(type='list', choices=['json', 'merge', 'strategic-merge'])
argument_spec['wait'] = dict(type='bool', default=False)
argument_spec['wait_sleep'] = dict(type='int', default=5)
argument_spec['wait_timeout'] = dict(type='int', default=120)
argument_spec['wait_condition'] = dict(type='dict', default=None, options=self.condition_spec)
argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec)
argument_spec['append_hash'] = dict(type='bool', default=False)
argument_spec['apply'] = dict(type='bool', default=False)
return argument_spec
def __init__(self, k8s_kind=None, *args, **kwargs):
self.client = None
self.warnings = []
mutually_exclusive = [
('resource_definition', 'src'),
('merge_type', 'apply'),
]
KubernetesAnsibleModule.__init__(self, *args,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
**kwargs)
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"):
self.fail_json(msg="openshift >= 0.8.0 is required for validate")
self.append_hash = self.params.get('append_hash')
if self.append_hash:
if not HAS_K8S_CONFIG_HASH:
self.fail_json(msg=missing_required_lib("openshift >= 0.7.2", reason="for append_hash"),
exception=K8S_CONFIG_HASH_IMP_ERR)
if self.params['merge_type']:
if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"):
self.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type"))
self.apply = self.params.get('apply', False)
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]
def flatten_list_kind(self, list_resource, definitions):
flattened = []
parent_api_version = list_resource.group_version if list_resource else None
parent_kind = list_resource.kind[:-4] if list_resource else None
for definition in definitions.get('items', []):
resource = self.find_resource(definition.get('kind', parent_kind), definition.get('apiVersion', parent_api_version), fail=True)
flattened.append((resource, self.set_defaults(resource, definition)))
return flattened
def execute_module(self):
changed = False
results = []
self.client = self.get_api_client()
flattened_definitions = []
for definition in self.resource_definitions:
kind = definition.get('kind', self.kind)
api_version = definition.get('apiVersion', self.api_version)
if kind.endswith('List'):
resource = self.find_resource(kind, api_version, fail=False)
flattened_definitions.extend(self.flatten_list_kind(resource, definition))
else:
resource = self.find_resource(kind, api_version, fail=True)
flattened_definitions.append((resource, definition))
for (resource, definition) in flattened_definitions:
kind = definition.get('kind', self.kind)
api_version = definition.get('apiVersion', self.api_version)
definition = self.set_defaults(resource, definition)
self.warnings = []
if self.params['validate'] is not None:
self.warnings = self.validate(definition)
result = self.perform_action(resource, definition)
result['warnings'] = self.warnings
changed = changed or result['changed']
results.append(result)
if len(results) == 1:
self.exit_json(**results[0])
self.exit_json(**{
'changed': changed,
'result': {
'results': results
}
})
def validate(self, resource):
def _prepend_resource_info(resource, msg):
return "%s %s: %s" % (resource['kind'], resource['metadata']['name'], msg)
try:
warnings, errors = self.client.validate(resource, self.params['validate'].get('version'), self.params['validate'].get('strict'))
except KubernetesValidateMissing:
self.fail_json(msg="kubernetes-validate python library is required to validate resources")
if errors and self.params['validate']['fail_on_error']:
self.fail_json(msg="\n".join([_prepend_resource_info(resource, error) for error in errors]))
else:
return [_prepend_resource_info(resource, msg) for msg in warnings + errors]
def set_defaults(self, resource, definition):
definition['kind'] = resource.kind
definition['apiVersion'] = resource.group_version
metadata = definition.get('metadata', {})
if self.name and not metadata.get('name'):
metadata['name'] = self.name
if resource.namespaced and self.namespace and not metadata.get('namespace'):
metadata['namespace'] = self.namespace
definition['metadata'] = metadata
return definition
def perform_action(self, resource, definition):
result = {'changed': False, 'result': {}}
state = self.params.get('state', None)
force = self.params.get('force', False)
name = definition['metadata'].get('name')
namespace = definition['metadata'].get('namespace')
existing = None
wait = self.params.get('wait')
wait_sleep = self.params.get('wait_sleep')
wait_timeout = self.params.get('wait_timeout')
wait_condition = None
if self.params.get('wait_condition') and self.params['wait_condition'].get('type'):
wait_condition = self.params['wait_condition']
self.remove_aliases()
try:
# ignore append_hash for resources other than ConfigMap and Secret
if self.append_hash and definition['kind'] in ['ConfigMap', 'Secret']:
name = '%s-%s' % (name, generate_hash(definition))
definition['metadata']['name'] = name
params = dict(name=name)
if namespace:
params['namespace'] = namespace
existing = resource.get(**params)
except NotFoundError:
# Remove traceback so that it doesn't show up in later failures
try:
sys.exc_clear()
except AttributeError:
# no sys.exc_clear on python3
pass
except ForbiddenError as exc:
if definition['kind'] in ['Project', 'ProjectRequest'] and state != 'absent':
return self.create_project_request(definition)
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
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)
if state == 'absent':
result['method'] = "delete"
if not existing:
# The object already does not exist
return result
else:
# Delete the object
result['changed'] = True
if not self.check_mode:
try:
k8s_obj = resource.delete(**params)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to delete object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
if wait:
success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent')
result['duration'] = duration
if not success:
self.fail_json(msg="Resource deletion timed out", **result)
return result
else:
if self.apply:
if self.check_mode:
ignored, k8s_obj = apply_object(resource, definition)
else:
try:
k8s_obj = resource.apply(definition, namespace=namespace).to_dict()
except DynamicApiError as exc:
msg = "Failed to apply object: {0}".format(exc.body)
if self.warnings:
msg += "\n" + "\n ".join(self.warnings)
self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
success = True
result['result'] = k8s_obj
if wait:
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
if existing:
existing = existing.to_dict()
else:
existing = {}
match, diffs = self.diff_objects(existing, result['result'])
result['changed'] = not match
result['diff'] = diffs
result['method'] = 'apply'
if not success:
self.fail_json(msg="Resource apply timed out", **result)
return result
if not existing:
if self.check_mode:
k8s_obj = definition
else:
try:
k8s_obj = resource.create(definition, namespace=namespace).to_dict()
except ConflictError:
# Some resources, like ProjectRequests, can't be created multiple times,
# because the resources that they create don't match their kind
# In this case we'll mark it as unchanged and warn the user
self.warn("{0} was not found, but creating it returned a 409 Conflict error. This can happen \
if the resource you are creating does not directly create a resource of the same kind.".format(name))
return result
except DynamicApiError as exc:
msg = "Failed to create object: {0}".format(exc.body)
if self.warnings:
msg += "\n" + "\n ".join(self.warnings)
self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
success = True
result['result'] = k8s_obj
if wait and not self.check_mode:
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
result['changed'] = True
result['method'] = 'create'
if not success:
self.fail_json(msg="Resource creation timed out", **result)
return result
match = False
diffs = []
if existing and force:
if self.check_mode:
k8s_obj = definition
else:
try:
k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=self.append_hash).to_dict()
except DynamicApiError as exc:
msg = "Failed to replace object: {0}".format(exc.body)
if self.warnings:
msg += "\n" + "\n ".join(self.warnings)
self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
success = True
result['result'] = k8s_obj
if wait:
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
result['method'] = 'replace'
result['diff'] = diffs
if not success:
self.fail_json(msg="Resource replacement timed out", **result)
return result
# Differences exist between the existing obj and requested params
if self.check_mode:
k8s_obj = dict_merge(existing.to_dict(), definition)
else:
if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"):
k8s_obj, error = self.patch_resource(resource, definition, existing, name,
namespace)
else:
for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']:
k8s_obj, error = self.patch_resource(resource, definition, existing, name,
namespace, merge_type=merge_type)
if not error:
break
if error:
self.fail_json(**error)
success = True
result['result'] = k8s_obj
if wait:
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
result['method'] = 'patch'
result['diff'] = diffs
if not success:
self.fail_json(msg="Resource update timed out", **result)
return result
def patch_resource(self, resource, definition, existing, name, namespace, merge_type=None):
try:
params = dict(name=name, namespace=namespace)
if merge_type:
params['content_type'] = 'application/{0}-patch+json'.format(merge_type)
k8s_obj = resource.patch(definition, **params).to_dict()
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
error = {}
return k8s_obj, {}
except DynamicApiError as exc:
msg = "Failed to patch object: {0}".format(exc.body)
if self.warnings:
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
def create_project_request(self, definition):
definition['kind'] = 'ProjectRequest'
result = {'changed': False, 'result': {}}
resource = self.find_resource('ProjectRequest', definition['apiVersion'], fail=True)
if not self.check_mode:
try:
k8s_obj = resource.create(definition)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to create object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
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

@@ -0,0 +1,243 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
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
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
SCALE_ARG_SPEC = {
'replicas': {'type': 'int', 'required': True},
'current_replicas': {'type': 'int'},
'resource_version': {},
'wait': {'type': 'bool', 'default': True},
'wait_timeout': {'type': 'int', 'default': 20},
}
class KubernetesAnsibleScaleModule(KubernetesAnsibleModule):
def __init__(self, k8s_kind=None, *args, **kwargs):
self.client = None
self.warnings = []
mutually_exclusive = [
('resource_definition', 'src'),
]
KubernetesAnsibleModule.__init__(self, *args,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
**kwargs)
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]
def execute_module(self):
definition = self.resource_definitions[0]
self.client = self.get_api_client()
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
api_version = definition['apiVersion']
kind = definition['kind']
current_replicas = self.params.get('current_replicas')
replicas = self.params.get('replicas')
resource_version = self.params.get('resource_version')
wait = self.params.get('wait')
wait_time = self.params.get('wait_timeout')
existing = None
existing_count = None
return_attributes = dict(changed=False, result=dict())
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:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
error=exc.value.get('status'))
if self.kind == 'job':
existing_count = existing.spec.parallelism
elif hasattr(existing.spec, 'replicas'):
existing_count = existing.spec.replicas
if existing_count is None:
self.fail_json(msg='Failed to retrieve the available count for the requested object.')
if resource_version and resource_version != existing.metadata.resourceVersion:
self.exit_json(**return_attributes)
if current_replicas is not None and existing_count != current_replicas:
self.exit_json(**return_attributes)
if existing_count != replicas:
return_attributes['changed'] = True
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
k8s_obj = resource.patch(existing.to_dict())
else:
k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
return_attributes['result'] = k8s_obj.to_dict()
self.exit_json(**return_attributes)
@property
def argspec(self):
args = copy.deepcopy(COMMON_ARG_SPEC)
args.pop('state')
args.pop('force')
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
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}}
return_obj = None
stream = None
if wait:
w, stream = self._create_stream(resource, namespace, wait_time)
try:
resource.scale.patch(body=scale_obj)
except Exception as 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)
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

21
plugins/modules/_oc.py Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2017, Kenneth D. Evensen <kevensen@redhat.com>
# 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': ['removed'],
'supported_by': 'community'
}
from ansible.module_utils.common.removed import removed_module
if __name__ == '__main__':
removed_module(removed_in='2.9')

View File

@@ -268,7 +268,7 @@ result:
sample: 48
'''
from ansible.module_utils.k8s.raw import KubernetesRawModule
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule
def main():

View File

@@ -131,7 +131,7 @@ resources:
'''
from ansible.module_utils.k8s.common import KubernetesAnsibleModule, AUTH_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule, AUTH_ARG_SPEC
import copy

View File

@@ -118,7 +118,7 @@ result:
type: complex
'''
from ansible.module_utils.k8s.scale import KubernetesAnsibleScaleModule
from ansible_collections.community.kubernetes.plugins.module_utils.scale import KubernetesAnsibleScaleModule
def main():

View File

@@ -167,8 +167,8 @@ import traceback
from collections import defaultdict
from ansible.module_utils.k8s.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible.module_utils.k8s.raw import KubernetesRawModule
from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule
SERVICE_ARG_SPEC = {

View File

@@ -1,2 +0,0 @@
dependencies:
- setup_remote_tmp_dir

View File

@@ -11,13 +11,9 @@ If kubectl is already using the right config file and context, you can
just do
```
cd test/integration/targets/k8s
cd tests/integration/targets/k8s
./runme.sh -vv
```
otherwise set one or both of `K8S_AUTH_KUBECONFIG` and `K8S_AUTH_CONTEXT`
and use the same command

View File

@@ -0,0 +1,5 @@
- name: delete temporary directory
file:
path: "{{ remote_tmp_dir }}"
state: absent
no_log: yes

View File

@@ -0,0 +1 @@
dependencies: []

View File

@@ -1,3 +1,5 @@
- include_tasks: setup_remote_tmp_dir.yml
- set_fact:
virtualenv: "{{ remote_tmp_dir }}/virtualenv"
virtualenv_command: "{{ ansible_python_interpreter }} -m virtualenv"

View File

@@ -0,0 +1,11 @@
- name: create temporary directory
tempfile:
state: directory
suffix: .test
register: remote_tmp_dir
notify:
- delete temporary directory
- name: record temporary directory
set_fact:
remote_tmp_dir: "{{ remote_tmp_dir.path }}"

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 63,
"targets": [
"k8s.k8s"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 19,
"targets": [
"k8s.k8s"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 19,
"targets": [
"k8s.k8s"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 20,
"targets": [
"kubernetes"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 19,
"targets": [
"kubernetes"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 19,
"targets": [
"kubernetes"
]
}
}
}

View File

@@ -0,0 +1,3 @@
{
"targets": {}
}

View File

@@ -0,0 +1,11 @@
{
"clouds": {
"openshift": {
"platform": "openshift",
"setup_seconds": 19,
"targets": [
"kubernetes"
]
}
}
}

View File

@@ -0,0 +1,39 @@
{
"targets": {
"kubernetes": {
"aliases": [
"cloud/",
"cloud/kubernetes",
"cloud/openshift/",
"cloud/openshift/kubernetes",
"kubernetes",
"module/",
"module/kubernetes/",
"module/kubernetes/kubernetes",
"non_destructive/",
"non_destructive/kubernetes",
"posix/",
"role/",
"role/kubernetes",
"shippable/",
"shippable/cloud/",
"shippable/cloud/group1/",
"shippable/cloud/group1/kubernetes",
"shippable/cloud/kubernetes",
"shippable/kubernetes"
],
"coverage": false,
"coverage_label": "",
"modules": [
"kubernetes"
],
"name": "kubernetes",
"python_version": "3.6",
"run_time_seconds": 455,
"setup_always": [],
"setup_once": [],
"type": "role",
"validation_seconds": 1
}
}
}

File diff suppressed because one or more lines are too long

6620
tests/sanity/ignore.txt Normal file

File diff suppressed because it is too large Load Diff