File introducing-the-kubernetes-module.patch of Package salt
From 902acf0b5ff837bc93b9a8efbb1f032810c3f35b Mon Sep 17 00:00:00 2001
From: Flavio Castelli <fcastelli@suse.com>
Date: Mon, 20 Feb 2017 14:28:54 +0100
Subject: [PATCH] Introducing the kubernetes module
Allows interaction with a kubernetes cluster. The following functions are implemented:
* kubernetes.ping
* kubernetes.nodes
* kubernetes.node
* kubernetes.node_add_label
* kubernetes.node_remove_label
* kubernetes.namespaces
* kubernetes.deployments
* kubernetes.services
* kubernetes.pods
* kubernetes.secrets
* kubernetes.configmaps
* kubernetes.show_deployment
* kubernetes.show_service
* kubernetes.show_pod
* kubernetes.show_namespace
* kubernetes.show_secret
* kubernetes.show_configmap
* kubernetes.delete_deployment
* kubernetes.delete_service
* kubernetes.delete_pod
* kubernetes.delete_namespace
* kubernetes.delete_secret
* kubernetes.delete_configmap
* kubernetes.create_deployment
* kubernetes.create_pod
* kubernetes.create_service
* kubernetes.create_secret
* kubernetes.create_configmap
* kubernetes.create_namespace
* kubernetes.replace_deployment
* kubernetes.replace_service
* kubernetes.replace_secret
* kubernetes.replace_configmap
Added unit tests:
* Node listing,
* deployment listing,
* service listing,
* pod listing,
* deployment deletion and
* deployment creation.
---
salt/modules/kubernetes.py | 1474 +++++++++++++++++++++++++++++++++
salt/states/k8s.py | 42 +
salt/states/kubernetes.py | 989 ++++++++++++++++++++++
tests/unit/modules/test_kubernetes.py | 125 +++
4 files changed, 2630 insertions(+)
create mode 100644 salt/modules/kubernetes.py
create mode 100644 salt/states/kubernetes.py
create mode 100644 tests/unit/modules/test_kubernetes.py
diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py
new file mode 100644
index 0000000000..d3d02f2e26
--- /dev/null
+++ b/salt/modules/kubernetes.py
@@ -0,0 +1,1474 @@
+# -*- coding: utf-8 -*-
+'''
+Module for handling kubernetes calls.
+
+:optdepends: - kubernetes Python client
+:configuration: The k8s API settings are provided either in a pillar, in
+ the minion's config file, or in master's config file::
+
+ kubernetes.user: admin
+ kubernetes.password: verybadpass
+ kubernetes.api_url: 'http://127.0.0.1:8080'
+ kubernetes.certificate-authority-data: '...'
+ kubernetes.client-certificate-data: '....n
+ kubernetes.client-key-data: '...'
+ kubernetes.certificate-authority-file: '/path/to/ca.crt'
+ kubernetes.client-certificate-file: '/path/to/client.crt'
+ kubernetes.client-key-file: '/path/to/client.key'
+
+
+These settings can be also overrided by adding `api_url`, `api_user`,
+`api_password`, `api_certificate_authority_file`, `api_client_certificate_file`
+or `api_client_key_file` parameters when calling a function:
+
+The data format for `kubernetes.*-data` values is the same as provided in `kubeconfig`.
+It's base64 encoded certificates/keys in one line.
+
+For an item only one field should be provided. Either a `data` or a `file` entry.
+In case both are provided the `file` entry is prefered.
+
+.. code-block:: bash
+ salt '*' kubernetes.nodes api_url=http://k8s-api-server:port api_user=myuser api_password=pass
+
+'''
+
+# Import Python Futures
+from __future__ import absolute_import
+import os.path
+import base64
+import logging
+import yaml
+import tempfile
+
+from salt.exceptions import CommandExecutionError
+from salt.ext.six import iteritems
+import salt.utils
+import salt.utils.templates
+
+try:
+ import kubernetes # pylint: disable=import-self
+ import kubernetes.client
+ from kubernetes.client.rest import ApiException
+ from urllib3.exceptions import HTTPError
+
+ HAS_LIBS = True
+except ImportError:
+ HAS_LIBS = False
+
+try:
+ # There is an API change in Kubernetes >= 2.0.0.
+ from kubernetes.client import V1beta1Deployment as AppsV1beta1Deployment
+ from kubernetes.client import V1beta1DeploymentSpec as AppsV1beta1DeploymentSpec
+except ImportError:
+ from kubernetes.client import AppsV1beta1Deployment
+ from kubernetes.client import AppsV1beta1DeploymentSpec
+
+
+log = logging.getLogger(__name__)
+
+__virtualname__ = 'kubernetes'
+
+
+def __virtual__():
+ '''
+ Check dependencies
+ '''
+ if HAS_LIBS:
+ return __virtualname__
+
+ return False, 'python kubernetes library not found'
+
+
+# pylint: disable=no-member
+def _setup_conn(**kwargs):
+ '''
+ Setup kubernetes API connection singleton
+ '''
+ host = __salt__['config.option']('kubernetes.api_url',
+ 'http://localhost:8080')
+ username = __salt__['config.option']('kubernetes.user')
+ password = __salt__['config.option']('kubernetes.password')
+ ca_cert = __salt__['config.option']('kubernetes.certificate-authority-data')
+ client_cert = __salt__['config.option']('kubernetes.client-certificate-data')
+ client_key = __salt__['config.option']('kubernetes.client-key-data')
+ ca_cert_file = __salt__['config.option']('kubernetes.certificate-authority-file')
+ client_cert_file = __salt__['config.option']('kubernetes.client-certificate-file')
+ client_key_file = __salt__['config.option']('kubernetes.client-key-file')
+
+ # Override default API settings when settings are provided
+ if 'api_url' in kwargs:
+ host = kwargs.get('api_url')
+
+ if 'api_user' in kwargs:
+ username = kwargs.get('api_user')
+
+ if 'api_password' in kwargs:
+ password = kwargs.get('api_password')
+
+ if 'api_certificate_authority_file' in kwargs:
+ ca_cert_file = kwargs.get('api_certificate_authority_file')
+
+ if 'api_client_certificate_file' in kwargs:
+ client_cert_file = kwargs.get('api_client_certificate_file')
+
+ if 'api_client_key_file' in kwargs:
+ client_key_file = kwargs.get('api_client_key_file')
+
+ if (
+ kubernetes.client.configuration.host != host or
+ kubernetes.client.configuration.user != username or
+ kubernetes.client.configuration.password != password):
+ # Recreates API connection if settings are changed
+ kubernetes.client.configuration.__init__()
+
+ kubernetes.client.configuration.host = host
+ kubernetes.client.configuration.user = username
+ kubernetes.client.configuration.passwd = password
+
+ if ca_cert_file:
+ kubernetes.client.configuration.ssl_ca_cert = ca_cert_file
+ elif ca_cert:
+ with tempfile.NamedTemporaryFile(prefix='salt-kube-', delete=False) as ca:
+ ca.write(base64.b64decode(ca_cert))
+ kubernetes.client.configuration.ssl_ca_cert = ca.name
+ else:
+ kubernetes.client.configuration.ssl_ca_cert = None
+
+ if client_cert_file:
+ kubernetes.client.configuration.cert_file = client_cert_file
+ elif client_cert:
+ with tempfile.NamedTemporaryFile(prefix='salt-kube-', delete=False) as c:
+ c.write(base64.b64decode(client_cert))
+ kubernetes.client.configuration.cert_file = c.name
+ else:
+ kubernetes.client.configuration.cert_file = None
+
+ if client_key_file:
+ kubernetes.client.configuration.key_file = client_key_file
+ if client_key:
+ with tempfile.NamedTemporaryFile(prefix='salt-kube-', delete=False) as k:
+ k.write(base64.b64decode(client_key))
+ kubernetes.client.configuration.key_file = k.name
+ else:
+ kubernetes.client.configuration.key_file = None
+
+
+def _cleanup(**kwargs):
+ ca = kubernetes.client.configuration.ssl_ca_cert
+ cert = kubernetes.client.configuration.cert_file
+ key = kubernetes.client.configuration.key_file
+ if cert and os.path.exists(cert) and os.path.basename(cert).startswith('salt-kube-'):
+ salt.utils.safe_rm(cert)
+ if key and os.path.exists(key) and os.path.basename(key).startswith('salt-kube-'):
+ salt.utils.safe_rm(key)
+ if ca and os.path.exists(ca) and os.path.basename(ca).startswith('salt-kube-'):
+ salt.utils.safe_rm(ca)
+
+
+def ping(**kwargs):
+ '''
+ Checks connections with the kubernetes API server.
+ Returns True if the connection can be established, False otherwise.
+
+ CLI Example:
+ salt '*' kubernetes.ping
+ '''
+ status = True
+ try:
+ nodes(**kwargs)
+ except CommandExecutionError:
+ status = False
+
+ return status
+
+
+def nodes(**kwargs):
+ '''
+ Return the names of the nodes composing the kubernetes cluster
+
+ CLI Examples::
+
+ salt '*' kubernetes.nodes
+ salt '*' kubernetes.nodes api_url=http://myhost:port api_user=my-user
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_node()
+
+ return [k8s_node['metadata']['name'] for k8s_node in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->list_node: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def node(name, **kwargs):
+ '''
+ Return the details of the node identified by the specified name
+
+ CLI Examples::
+
+ salt '*' kubernetes.node name='minikube'
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_node()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->list_node: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+ for k8s_node in api_response.items:
+ if k8s_node.metadata.name == name:
+ return k8s_node.to_dict()
+
+ return None
+
+
+def node_labels(name, **kwargs):
+ '''
+ Return the labels of the node identified by the specified name
+
+ CLI Examples::
+
+ salt '*' kubernetes.node_labels name="minikube"
+ '''
+ match = node(name, **kwargs)
+
+ if match is not None:
+ return match.metadata.labels
+
+ return {}
+
+
+def node_add_label(node_name, label_name, label_value, **kwargs):
+ '''
+ Set the value of the label identified by `label_name` to `label_value` on
+ the node identified by the name `node_name`.
+ Creates the lable if not present.
+
+ CLI Examples::
+
+ salt '*' kubernetes.node_add_label node_name="minikube" \
+ label_name="foo" label_value="bar"
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ body = {
+ 'metadata': {
+ 'labels': {
+ label_name: label_value}
+ }
+ }
+ api_response = api_instance.patch_node(node_name, body)
+ return api_response
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->patch_node: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+ return None
+
+
+def node_remove_label(node_name, label_name, **kwargs):
+ '''
+ Removes the label identified by `label_name` from
+ the node identified by the name `node_name`.
+
+ CLI Examples::
+
+ salt '*' kubernetes.node_remove_label node_name="minikube" \
+ label_name="foo"
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ body = {
+ 'metadata': {
+ 'labels': {
+ label_name: None}
+ }
+ }
+ api_response = api_instance.patch_node(node_name, body)
+ return api_response
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->patch_node: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+ return None
+
+
+def namespaces(**kwargs):
+ '''
+ Return the names of the available namespaces
+
+ CLI Examples::
+
+ salt '*' kubernetes.namespaces
+ salt '*' kubernetes.namespaces api_url=http://myhost:port api_user=my-user
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_namespace()
+
+ return [nms['metadata']['name'] for nms in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->list_namespace: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def deployments(namespace='default', **kwargs):
+ '''
+ Return a list of kubernetes deployments defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.deployments
+ salt '*' kubernetes.deployments namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.ExtensionsV1beta1Api()
+ api_response = api_instance.list_namespaced_deployment(namespace)
+
+ return [dep['metadata']['name'] for dep in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'ExtensionsV1beta1Api->list_namespaced_deployment: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def services(namespace='default', **kwargs):
+ '''
+ Return a list of kubernetes services defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.services
+ salt '*' kubernetes.services namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_namespaced_service(namespace)
+
+ return [srv['metadata']['name'] for srv in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->list_namespaced_service: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def pods(namespace='default', **kwargs):
+ '''
+ Return a list of kubernetes pods defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.pods
+ salt '*' kubernetes.pods namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_namespaced_pod(namespace)
+
+ return [pod['metadata']['name'] for pod in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->list_namespaced_pod: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def secrets(namespace='default', **kwargs):
+ '''
+ Return a list of kubernetes secrets defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.secrets
+ salt '*' kubernetes.secrets namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_namespaced_secret(namespace)
+
+ return [secret['metadata']['name'] for secret in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->list_namespaced_secret: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def configmaps(namespace='default', **kwargs):
+ '''
+ Return a list of kubernetes configmaps defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.configmaps
+ salt '*' kubernetes.configmaps namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.list_namespaced_config_map(namespace)
+
+ return [secret['metadata']['name'] for secret in api_response.to_dict().get('items')]
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->list_namespaced_config_map: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_deployment(name, namespace='default', **kwargs):
+ '''
+ Return the kubernetes deployment defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_deployment my-nginx default
+ salt '*' kubernetes.show_deployment name=my-nginx namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.ExtensionsV1beta1Api()
+ api_response = api_instance.read_namespaced_deployment(name, namespace)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'ExtensionsV1beta1Api->read_namespaced_deployment: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_service(name, namespace='default', **kwargs):
+ '''
+ Return the kubernetes service defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_service my-nginx default
+ salt '*' kubernetes.show_service name=my-nginx namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.read_namespaced_service(name, namespace)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->read_namespaced_service: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_pod(name, namespace='default', **kwargs):
+ '''
+ Return POD information for a given pod name defined in the namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_pod guestbook-708336848-fqr2x
+ salt '*' kubernetes.show_pod guestbook-708336848-fqr2x namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.read_namespaced_pod(name, namespace)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->read_namespaced_pod: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_namespace(name, **kwargs):
+ '''
+ Return information for a given namespace defined by the specified name
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_namespace kube-system
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.read_namespace(name)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->read_namespace: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_secret(name, namespace='default', decode=False, **kwargs):
+ '''
+ Return the kubernetes secret defined by name and namespace.
+ The secrets can be decoded if specified by the user. Warning: this has
+ security implications.
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_secret confidential default
+ salt '*' kubernetes.show_secret name=confidential namespace=default
+ salt '*' kubernetes.show_secret name=confidential decode=True
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.read_namespaced_secret(name, namespace)
+
+ if api_response.data and (decode or decode == 'True'):
+ for key in api_response.data:
+ value = api_response.data[key]
+ api_response.data[key] = base64.b64decode(value)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->read_namespaced_secret: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def show_configmap(name, namespace='default', **kwargs):
+ '''
+ Return the kubernetes configmap defined by name and namespace.
+
+ CLI Examples::
+
+ salt '*' kubernetes.show_configmap game-config default
+ salt '*' kubernetes.show_configmap name=game-config namespace=default
+ '''
+ _setup_conn(**kwargs)
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.read_namespaced_config_map(
+ name,
+ namespace)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->read_namespaced_config_map: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_deployment(name, namespace='default', **kwargs):
+ '''
+ Deletes the kubernetes deployment defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_deployment my-nginx
+ salt '*' kubernetes.delete_deployment name=my-nginx namespace=default
+ '''
+ _setup_conn(**kwargs)
+ body = kubernetes.client.V1DeleteOptions(orphan_dependents=True)
+
+ try:
+ api_instance = kubernetes.client.ExtensionsV1beta1Api()
+ api_response = api_instance.delete_namespaced_deployment(
+ name=name,
+ namespace=namespace,
+ body=body)
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'ExtensionsV1beta1Api->delete_namespaced_deployment: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_service(name, namespace='default', **kwargs):
+ '''
+ Deletes the kubernetes service defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_service my-nginx default
+ salt '*' kubernetes.delete_service name=my-nginx namespace=default
+ '''
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.delete_namespaced_service(
+ name=name,
+ namespace=namespace)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->delete_namespaced_service: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_pod(name, namespace='default', **kwargs):
+ '''
+ Deletes the kubernetes pod defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_pod guestbook-708336848-5nl8c default
+ salt '*' kubernetes.delete_pod name=guestbook-708336848-5nl8c namespace=default
+ '''
+ _setup_conn(**kwargs)
+ body = kubernetes.client.V1DeleteOptions(orphan_dependents=True)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.delete_namespaced_pod(
+ name=name,
+ namespace=namespace,
+ body=body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->delete_namespaced_pod: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_namespace(name, **kwargs):
+ '''
+ Deletes the kubernetes namespace defined by name
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_namespace salt
+ salt '*' kubernetes.delete_namespace name=salt
+ '''
+ _setup_conn(**kwargs)
+ body = kubernetes.client.V1DeleteOptions(orphan_dependents=True)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.delete_namespace(name=name, body=body)
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->delete_namespace: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_secret(name, namespace='default', **kwargs):
+ '''
+ Deletes the kubernetes secret defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_secret confidential default
+ salt '*' kubernetes.delete_secret name=confidential namespace=default
+ '''
+ _setup_conn(**kwargs)
+ body = kubernetes.client.V1DeleteOptions(orphan_dependents=True)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.delete_namespaced_secret(
+ name=name,
+ namespace=namespace,
+ body=body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling CoreV1Api->delete_namespaced_secret: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def delete_configmap(name, namespace='default', **kwargs):
+ '''
+ Deletes the kubernetes configmap defined by name and namespace
+
+ CLI Examples::
+
+ salt '*' kubernetes.delete_configmap settings default
+ salt '*' kubernetes.delete_configmap name=settings namespace=default
+ '''
+ _setup_conn(**kwargs)
+ body = kubernetes.client.V1DeleteOptions(orphan_dependents=True)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.delete_namespaced_config_map(
+ name=name,
+ namespace=namespace,
+ body=body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->delete_namespaced_config_map: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_deployment(
+ name,
+ namespace,
+ metadata,
+ spec,
+ source,
+ template,
+ saltenv,
+ **kwargs):
+ '''
+ Creates the kubernetes deployment as defined by the user.
+ '''
+ body = __create_object_body(
+ kind='Deployment',
+ obj_class=AppsV1beta1Deployment,
+ spec_creator=__dict_to_deployment_spec,
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=saltenv)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.ExtensionsV1beta1Api()
+ api_response = api_instance.create_namespaced_deployment(
+ namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'ExtensionsV1beta1Api->create_namespaced_deployment: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_pod(
+ name,
+ namespace,
+ metadata,
+ spec,
+ source,
+ template,
+ saltenv,
+ **kwargs):
+ '''
+ Creates the kubernetes deployment as defined by the user.
+ '''
+ body = __create_object_body(
+ kind='Pod',
+ obj_class=kubernetes.client.V1Pod,
+ spec_creator=__dict_to_pod_spec,
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=saltenv)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.create_namespaced_pod(
+ namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->create_namespaced_pod: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_service(
+ name,
+ namespace,
+ metadata,
+ spec,
+ source,
+ template,
+ saltenv,
+ **kwargs):
+ '''
+ Creates the kubernetes service as defined by the user.
+ '''
+ body = __create_object_body(
+ kind='Service',
+ obj_class=kubernetes.client.V1Service,
+ spec_creator=__dict_to_service_spec,
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=saltenv)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.create_namespaced_service(
+ namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->create_namespaced_service: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_secret(
+ name,
+ namespace,
+ data,
+ source,
+ template,
+ saltenv,
+ **kwargs):
+ '''
+ Creates the kubernetes secret as defined by the user.
+ '''
+ if source:
+ data = __read_and_render_yaml_file(source, template, saltenv)
+ elif data is None:
+ data = {}
+
+ data = __enforce_only_strings_dict(data)
+
+ # encode the secrets using base64 as required by kubernetes
+ for key in data:
+ data[key] = base64.b64encode(data[key])
+
+ body = kubernetes.client.V1Secret(
+ metadata=__dict_to_object_meta(name, namespace, {}),
+ data=data)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.create_namespaced_secret(
+ namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->create_namespaced_secret: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_configmap(
+ name,
+ namespace,
+ data,
+ source,
+ template,
+ saltenv,
+ **kwargs):
+ '''
+ Creates the kubernetes configmap as defined by the user.
+ '''
+ if source:
+ data = __read_and_render_yaml_file(source, template, saltenv)
+ elif data is None:
+ data = {}
+
+ data = __enforce_only_strings_dict(data)
+
+ body = kubernetes.client.V1ConfigMap(
+ metadata=__dict_to_object_meta(name, namespace, {}),
+ data=data)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.create_namespaced_config_map(
+ namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->create_namespaced_config_map: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def create_namespace(
+ name,
+ **kwargs):
+ '''
+ Creates a namespace with the specified name.
+
+ CLI Example:
+ salt '*' kubernetes.create_namespace salt
+ salt '*' kubernetes.create_namespace name=salt
+ '''
+
+ meta_obj = kubernetes.client.V1ObjectMeta(name=name)
+ body = kubernetes.client.V1Namespace(metadata=meta_obj)
+ body.metadata.name = name
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.create_namespace(body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->create_namespace: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def replace_deployment(name,
+ metadata,
+ spec,
+ source,
+ template,
+ saltenv,
+ namespace='default',
+ **kwargs):
+ '''
+ Replaces an existing deployment with a new one defined by name and
+ namespace, having the specificed metadata and spec.
+ '''
+ body = __create_object_body(
+ kind='Deployment',
+ obj_class=AppsV1beta1Deployment,
+ spec_creator=__dict_to_deployment_spec,
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=saltenv)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.ExtensionsV1beta1Api()
+ api_response = api_instance.replace_namespaced_deployment(
+ name, namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'ExtensionsV1beta1Api->replace_namespaced_deployment: '
+ '{0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def replace_service(name,
+ metadata,
+ spec,
+ source,
+ template,
+ old_service,
+ saltenv,
+ namespace='default',
+ **kwargs):
+ '''
+ Replaces an existing service with a new one defined by name and namespace,
+ having the specificed metadata and spec.
+ '''
+ body = __create_object_body(
+ kind='Service',
+ obj_class=kubernetes.client.V1Service,
+ spec_creator=__dict_to_service_spec,
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=saltenv)
+
+ # Some attributes have to be preserved
+ # otherwise exceptions will be thrown
+ body.spec.cluster_ip = old_service['spec']['cluster_ip']
+ body.metadata.resource_version = old_service['metadata']['resource_version']
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.replace_namespaced_service(
+ name, namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->replace_namespaced_service: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def replace_secret(name,
+ data,
+ source,
+ template,
+ saltenv,
+ namespace='default',
+ **kwargs):
+ '''
+ Replaces an existing secret with a new one defined by name and namespace,
+ having the specificed data.
+ '''
+ if source:
+ data = __read_and_render_yaml_file(source, template, saltenv)
+ elif data is None:
+ data = {}
+
+ data = __enforce_only_strings_dict(data)
+
+ # encode the secrets using base64 as required by kubernetes
+ for key in data:
+ data[key] = base64.b64encode(data[key])
+
+ body = kubernetes.client.V1Secret(
+ metadata=__dict_to_object_meta(name, namespace, {}),
+ data=data)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.replace_namespaced_secret(
+ name, namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->replace_namespaced_secret: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def replace_configmap(name,
+ data,
+ source,
+ template,
+ saltenv,
+ namespace='default',
+ **kwargs):
+ '''
+ Replaces an existing configmap with a new one defined by name and
+ namespace, having the specificed data.
+ '''
+ if source:
+ data = __read_and_render_yaml_file(source, template, saltenv)
+
+ data = __enforce_only_strings_dict(data)
+
+ body = kubernetes.client.V1ConfigMap(
+ metadata=__dict_to_object_meta(name, namespace, {}),
+ data=data)
+
+ _setup_conn(**kwargs)
+
+ try:
+ api_instance = kubernetes.client.CoreV1Api()
+ api_response = api_instance.replace_namespaced_config_map(
+ name, namespace, body)
+
+ return api_response.to_dict()
+ except (ApiException, HTTPError) as exc:
+ if isinstance(exc, ApiException) and exc.status == 404:
+ return None
+ else:
+ log.exception(
+ 'Exception when calling '
+ 'CoreV1Api->replace_namespaced_configmap: {0}'.format(exc)
+ )
+ raise CommandExecutionError(exc)
+ finally:
+ _cleanup()
+
+
+def __create_object_body(kind,
+ obj_class,
+ spec_creator,
+ name,
+ namespace,
+ metadata,
+ spec,
+ source,
+ template,
+ saltenv):
+ '''
+ Create a Kubernetes Object body instance.
+ '''
+ if source:
+ src_obj = __read_and_render_yaml_file(source, template, saltenv)
+ if (
+ not isinstance(src_obj, dict) or
+ 'kind' not in src_obj or
+ src_obj['kind'] != kind):
+ raise CommandExecutionError(
+ 'The source file should define only '
+ 'a {0} object'.format(kind))
+
+ if 'metadata' in src_obj:
+ metadata = src_obj['metadata']
+ if 'spec' in src_obj:
+ spec = src_obj['spec']
+
+ return obj_class(
+ metadata=__dict_to_object_meta(name, namespace, metadata),
+ spec=spec_creator(spec))
+
+
+def __read_and_render_yaml_file(source,
+ template,
+ saltenv):
+ '''
+ Read a yaml file and, if needed, renders that using the specifieds
+ templating. Returns the python objects defined inside of the file.
+ '''
+ sfn = __salt__['cp.cache_file'](source, saltenv)
+ if not sfn:
+ raise CommandExecutionError(
+ 'Source file \'{0}\' not found'.format(source))
+
+ with salt.utils.fopen(sfn, 'r') as src:
+ contents = src.read()
+
+ if template:
+ if template in salt.utils.templates.TEMPLATE_REGISTRY:
+ # TODO: should we allow user to set also `context` like # pylint: disable=fixme
+ # `file.managed` does?
+ # Apply templating
+ data = salt.utils.templates.TEMPLATE_REGISTRY[template](
+ contents,
+ from_str=True,
+ to_str=True,
+ saltenv=saltenv,
+ grains=__grains__,
+ pillar=__pillar__,
+ salt=__salt__,
+ opts=__opts__)
+
+ if not data['result']:
+ # Failed to render the template
+ raise CommandExecutionError(
+ 'Failed to render file path with error: '
+ '{0}'.format(data['data'])
+ )
+
+ contents = data['data'].encode('utf-8')
+ else:
+ raise CommandExecutionError(
+ 'Unknown template specified: {0}'.format(
+ template))
+
+ return yaml.load(contents)
+
+
+def __dict_to_object_meta(name, namespace, metadata):
+ '''
+ Converts a dictionary into kubernetes ObjectMetaV1 instance.
+ '''
+ meta_obj = kubernetes.client.V1ObjectMeta()
+ meta_obj.namespace = namespace
+ for key, value in iteritems(metadata):
+ if hasattr(meta_obj, key):
+ setattr(meta_obj, key, value)
+
+ if meta_obj.name != name:
+ log.warning(
+ 'The object already has a name attribute, overwriting it with '
+ 'the one defined inside of salt')
+ meta_obj.name = name
+
+ return meta_obj
+
+
+def __dict_to_deployment_spec(spec):
+ '''
+ Converts a dictionary into kubernetes AppsV1beta1DeploymentSpec instance.
+ '''
+ spec_obj = AppsV1beta1DeploymentSpec()
+ for key, value in iteritems(spec):
+ if hasattr(spec_obj, key):
+ setattr(spec_obj, key, value)
+
+ return spec_obj
+
+
+def __dict_to_pod_spec(spec):
+ '''
+ Converts a dictionary into kubernetes V1PodSpec instance.
+ '''
+ spec_obj = kubernetes.client.V1PodSpec()
+ for key, value in iteritems(spec):
+ if hasattr(spec_obj, key):
+ setattr(spec_obj, key, value)
+
+ return spec_obj
+
+
+def __dict_to_service_spec(spec):
+ '''
+ Converts a dictionary into kubernetes V1ServiceSpec instance.
+ '''
+ spec_obj = kubernetes.client.V1ServiceSpec()
+ for key, value in iteritems(spec): # pylint: disable=too-many-nested-blocks
+ if key == 'ports':
+ spec_obj.ports = []
+ for port in value:
+ kube_port = kubernetes.client.V1ServicePort()
+ if isinstance(port, dict):
+ for port_key, port_value in iteritems(port):
+ if hasattr(kube_port, port_key):
+ setattr(kube_port, port_key, port_value)
+ else:
+ kube_port.port = port
+ spec_obj.ports.append(kube_port)
+ elif hasattr(spec_obj, key):
+ setattr(spec_obj, key, value)
+
+ return spec_obj
+
+
+def __enforce_only_strings_dict(dictionary):
+ '''
+ Returns a dictionary that has string keys and values.
+ '''
+ ret = {}
+
+ for key, value in iteritems(dictionary):
+ ret[str(key)] = str(value)
+
+ return ret
diff --git a/salt/states/k8s.py b/salt/states/k8s.py
index 879843be17..9c64c558ae 100644
--- a/salt/states/k8s.py
+++ b/salt/states/k8s.py
@@ -25,6 +25,11 @@ Manage Kubernetes
- node: myothernodename
- apiserver: http://mykubeapiserer:8080
'''
+from __future__ import absolute_import
+
+# Import salt libs
+import salt.utils
+
__virtualname__ = 'k8s'
@@ -42,6 +47,10 @@ def label_present(
node=None,
apiserver=None):
'''
+ .. deprecated:: Nitrogen
+ This state has been moved to :py:func:`kubernetes.node_label_present
+ <salt.states.kubernetes.node_label_present`.
+
Ensure the label exists on the kube node.
name
@@ -60,6 +69,14 @@ def label_present(
# Use salt k8s module to set label
ret = __salt__['k8s.label_present'](name, value, node, apiserver)
+ msg = (
+ 'The k8s.label_present state has been replaced by '
+ 'kubernetes.node_label_present. Update your SLS to use the new '
+ 'function name to get rid of this warning.'
+ )
+ salt.utils.warn_until('Fluorine', msg)
+ ret.setdefault('warnings', []).append(msg)
+
return ret
@@ -68,6 +85,10 @@ def label_absent(
node=None,
apiserver=None):
'''
+ .. deprecated:: Nitrogen
+ This state has been moved to :py:func:`kubernetes.node_label_absent
+ <salt.states.kubernetes.node_label_absent`.
+
Ensure the label doesn't exist on the kube node.
name
@@ -83,6 +104,14 @@ def label_absent(
# Use salt k8s module to set label
ret = __salt__['k8s.label_absent'](name, node, apiserver)
+ msg = (
+ 'The k8s.label_absent state has been replaced by '
+ 'kubernetes.node_label_absent. Update your SLS to use the new '
+ 'function name to get rid of this warning.'
+ )
+ salt.utils.warn_until('Fluorine', msg)
+ ret.setdefault('warnings', []).append(msg)
+
return ret
@@ -91,6 +120,10 @@ def label_folder_absent(
node=None,
apiserver=None):
'''
+ .. deprecated:: Nitrogen
+ This state has been moved to :py:func:`kubernetes.node_label_folder_absent
+ <salt.states.kubernetes.node_label_folder_absent`.
+
Ensure the label folder doesn't exist on the kube node.
name
@@ -106,4 +139,13 @@ def label_folder_absent(
# Use salt k8s module to set label
ret = __salt__['k8s.folder_absent'](name, node, apiserver)
+ msg = (
+ 'The k8s.label_folder_absent state has been replaced by '
+ 'kubernetes.node_label_folder_absent. Update your SLS to use the new '
+ 'function name to get rid of this warning.'
+
+ )
+ salt.utils.warn_until('Fluorine', msg)
+ ret.setdefault('warnings', []).append(msg)
+
return ret
diff --git a/salt/states/kubernetes.py b/salt/states/kubernetes.py
new file mode 100644
index 0000000000..64cd451532
--- /dev/null
+++ b/salt/states/kubernetes.py
@@ -0,0 +1,989 @@
+# -*- coding: utf-8 -*-
+'''
+Manage kubernetes resources as salt states
+==========================================
+
+NOTE: This module requires the proper pillar values set. See
+salt.modules.kubernetes for more information.
+
+The kubernetes module is used to manage different kubernetes resources.
+
+
+.. code-block:: yaml
+
+ my-nginx:
+ kubernetes.deployment_present:
+ - namespace: default
+ metadata:
+ app: frontend
+ spec:
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ run: my-nginx
+ spec:
+ containers:
+ - name: my-nginx
+ image: nginx
+ ports:
+ - containerPort: 80
+
+ my-mariadb:
+ kubernetes.deployment_absent:
+ - namespace: default
+
+ # kubernetes deployment as specified inside of
+ # a file containing the definition of the the
+ # deployment using the official kubernetes format
+ redis-master-deployment:
+ kubernetes.deployment_present:
+ - name: redis-master
+ - source: salt://k8s/redis-master-deployment.yml
+ require:
+ - pip: kubernetes-python-module
+
+ # kubernetes service as specified inside of
+ # a file containing the definition of the the
+ # service using the official kubernetes format
+ redis-master-service:
+ kubernetes.service_present:
+ - name: redis-master
+ - source: salt://k8s/redis-master-service.yml
+ require:
+ - kubernetes.deployment_present: redis-master
+
+ # kubernetes deployment as specified inside of
+ # a file containing the definition of the the
+ # deployment using the official kubernetes format
+ # plus some jinja directives
+ nginx-source-template:
+ kubernetes.deployment_present:
+ - source: salt://k8s/nginx.yml.jinja
+ - template: jinja
+ require:
+ - pip: kubernetes-python-module
+
+
+ # Kubernetes secret
+ k8s-secret:
+ kubernetes.secret_present:
+ - name: top-secret
+ data:
+ key1: value1
+ key2: value2
+ key3: value3
+'''
+from __future__ import absolute_import
+
+import copy
+import logging
+log = logging.getLogger(__name__)
+
+
+def __virtual__():
+ '''
+ Only load if the kubernetes module is available in __salt__
+ '''
+ return 'kubernetes.ping' in __salt__
+
+
+def _error(ret, err_msg):
+ '''
+ Helper function to propagate errors to
+ the end user.
+ '''
+ ret['result'] = False
+ ret['comment'] = err_msg
+ return ret
+
+
+def deployment_absent(name, namespace='default', **kwargs):
+ '''
+ Ensures that the named deployment is absent from the given namespace.
+
+ name
+ The name of the deployment
+
+ namespace
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ deployment = __salt__['kubernetes.show_deployment'](name, namespace, **kwargs)
+
+ if deployment is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The deployment does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The deployment is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ res = __salt__['kubernetes.delete_deployment'](name, namespace, **kwargs)
+ if res['code'] == 200:
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.deployment': {
+ 'new': 'absent', 'old': 'present'}}
+ ret['comment'] = res['message']
+ else:
+ ret['comment'] = 'Something went wrong, response: {0}'.format(res)
+
+ return ret
+
+
+def deployment_present(
+ name,
+ namespace='default',
+ metadata=None,
+ spec=None,
+ source='',
+ template='',
+ **kwargs):
+ '''
+ Ensures that the named deployment is present inside of the specified
+ namespace with the given metadata and spec.
+ If the deployment exists it will be replaced.
+
+ name
+ The name of the deployment.
+
+ namespace
+ The namespace holding the deployment. The 'default' one is going to be
+ used unless a different one is specified.
+
+ metadata
+ The metadata of the deployment object.
+
+ spec
+ The spec of the deployment object.
+
+ source
+ A file containing the definition of the deployment (metadata and
+ spec) in the official kubernetes format.
+
+ template
+ Template engine to be used to render the source file.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ if (metadata or spec) and source:
+ return _error(
+ ret,
+ '\'source\' cannot be used in combination with \'metadata\' or '
+ '\'spec\''
+ )
+
+ if metadata is None:
+ metadata = {}
+
+ if spec is None:
+ spec = {}
+
+ deployment = __salt__['kubernetes.show_deployment'](name, namespace, **kwargs)
+
+ if deployment is None:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The deployment is going to be created'
+ return ret
+ res = __salt__['kubernetes.create_deployment'](name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+ ret['changes']['{0}.{1}'.format(namespace, name)] = {
+ 'old': {},
+ 'new': res}
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ # TODO: improve checks # pylint: disable=fixme
+ log.info('Forcing the recreation of the deployment')
+ ret['comment'] = 'The deployment is already present. Forcing recreation'
+ res = __salt__['kubernetes.replace_deployment'](
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+
+ ret['changes'] = {
+ 'metadata': metadata,
+ 'spec': spec
+ }
+ ret['result'] = True
+ return ret
+
+
+def service_present(
+ name,
+ namespace='default',
+ metadata=None,
+ spec=None,
+ source='',
+ template='',
+ **kwargs):
+ '''
+ Ensures that the named service is present inside of the specified namespace
+ with the given metadata and spec.
+ If the deployment exists it will be replaced.
+
+ name
+ The name of the service.
+
+ namespace
+ The namespace holding the service. The 'default' one is going to be
+ used unless a different one is specified.
+
+ metadata
+ The metadata of the service object.
+
+ spec
+ The spec of the service object.
+
+ source
+ A file containing the definition of the service (metadata and
+ spec) in the official kubernetes format.
+
+ template
+ Template engine to be used to render the source file.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ if (metadata or spec) and source:
+ return _error(
+ ret,
+ '\'source\' cannot be used in combination with \'metadata\' or '
+ '\'spec\''
+ )
+
+ if metadata is None:
+ metadata = {}
+
+ if spec is None:
+ spec = {}
+
+ service = __salt__['kubernetes.show_service'](name, namespace, **kwargs)
+
+ if service is None:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The service is going to be created'
+ return ret
+ res = __salt__['kubernetes.create_service'](name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+ ret['changes']['{0}.{1}'.format(namespace, name)] = {
+ 'old': {},
+ 'new': res}
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ # TODO: improve checks # pylint: disable=fixme
+ log.info('Forcing the recreation of the service')
+ ret['comment'] = 'The service is already present. Forcing recreation'
+ res = __salt__['kubernetes.replace_service'](
+ name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ old_service=service,
+ saltenv=__env__,
+ **kwargs)
+
+ ret['changes'] = {
+ 'metadata': metadata,
+ 'spec': spec
+ }
+ ret['result'] = True
+ return ret
+
+
+def service_absent(name, namespace='default', **kwargs):
+ '''
+ Ensures that the named service is absent from the given namespace.
+
+ name
+ The name of the service
+
+ namespace
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ service = __salt__['kubernetes.show_service'](name, namespace, **kwargs)
+
+ if service is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The service does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The service is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ res = __salt__['kubernetes.delete_service'](name, namespace, **kwargs)
+ if res['code'] == 200:
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.service': {
+ 'new': 'absent', 'old': 'present'}}
+ ret['comment'] = res['message']
+ else:
+ ret['comment'] = 'Something went wrong, response: {0}'.format(res)
+
+ return ret
+
+
+def namespace_absent(name, **kwargs):
+ '''
+ Ensures that the named namespace is absent.
+
+ name
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ namespace = __salt__['kubernetes.show_namespace'](name, **kwargs)
+
+ if namespace is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The namespace does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The namespace is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ res = __salt__['kubernetes.delete_namespace'](name, **kwargs)
+ if (
+ res['code'] == 200 or
+ (
+ isinstance(res['status'], str) and
+ 'Terminating' in res['status']
+ ) or
+ (
+ isinstance(res['status'], dict) and
+ res['status']['phase'] == 'Terminating'
+ )):
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.namespace': {
+ 'new': 'absent', 'old': 'present'}}
+ if res['message']:
+ ret['comment'] = res['message']
+ else:
+ ret['comment'] = 'Terminating'
+ else:
+ ret['comment'] = 'Something went wrong, response: {0}'.format(res)
+
+ return ret
+
+
+def namespace_present(name, **kwargs):
+ '''
+ Ensures that the named namespace is present.
+
+ name
+ The name of the deployment.
+
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ namespace = __salt__['kubernetes.show_namespace'](name, **kwargs)
+
+ if namespace is None:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The namespace is going to be created'
+ return ret
+
+ res = __salt__['kubernetes.create_namespace'](name, **kwargs)
+ ret['changes']['namespace'] = {
+ 'old': {},
+ 'new': res}
+ else:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The namespace already exists'
+
+ return ret
+
+
+def secret_absent(name, namespace='default', **kwargs):
+ '''
+ Ensures that the named secret is absent from the given namespace.
+
+ name
+ The name of the secret
+
+ namespace
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ secret = __salt__['kubernetes.show_secret'](name, namespace, **kwargs)
+
+ if secret is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The secret does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The secret is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ __salt__['kubernetes.delete_secret'](name, namespace, **kwargs)
+
+ # As for kubernetes 1.6.4 doesn't set a code when deleting a secret
+ # The kubernetes module will raise an exception if the kubernetes
+ # server will return an error
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.secret': {
+ 'new': 'absent', 'old': 'present'}}
+ ret['comment'] = 'Secret deleted'
+ return ret
+
+
+def secret_present(
+ name,
+ namespace='default',
+ data=None,
+ source='',
+ template='',
+ **kwargs):
+ '''
+ Ensures that the named secret is present inside of the specified namespace
+ with the given data.
+ If the secret exists it will be replaced.
+
+ name
+ The name of the secret.
+
+ namespace
+ The namespace holding the secret. The 'default' one is going to be
+ used unless a different one is specified.
+
+ data
+ The dictionary holding the secrets.
+
+ source
+ A file containing the data of the secret in plain format.
+
+ template
+ Template engine to be used to render the source file.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ if data and source:
+ return _error(
+ ret,
+ '\'source\' cannot be used in combination with \'data\''
+ )
+
+ secret = __salt__['kubernetes.show_secret'](name, namespace, **kwargs)
+
+ if secret is None:
+ if data is None:
+ data = {}
+
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The secret is going to be created'
+ return ret
+ res = __salt__['kubernetes.create_secret'](name=name,
+ namespace=namespace,
+ data=data,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+ ret['changes']['{0}.{1}'.format(namespace, name)] = {
+ 'old': {},
+ 'new': res}
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ # TODO: improve checks # pylint: disable=fixme
+ log.info('Forcing the recreation of the service')
+ ret['comment'] = 'The secret is already present. Forcing recreation'
+ res = __salt__['kubernetes.replace_secret'](
+ name=name,
+ namespace=namespace,
+ data=data,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+
+ ret['changes'] = {
+ # Omit values from the return. They are unencrypted
+ # and can contain sensitive data.
+ 'data': res['data'].keys()
+ }
+ ret['result'] = True
+
+ return ret
+
+
+def configmap_absent(name, namespace='default', **kwargs):
+ '''
+ Ensures that the named configmap is absent from the given namespace.
+
+ name
+ The name of the configmap
+
+ namespace
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ configmap = __salt__['kubernetes.show_configmap'](name, namespace, **kwargs)
+
+ if configmap is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The configmap does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The configmap is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ __salt__['kubernetes.delete_configmap'](name, namespace, **kwargs)
+ # As for kubernetes 1.6.4 doesn't set a code when deleting a configmap
+ # The kubernetes module will raise an exception if the kubernetes
+ # server will return an error
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.configmap': {
+ 'new': 'absent', 'old': 'present'}}
+ ret['comment'] = 'ConfigMap deleted'
+
+ return ret
+
+
+def configmap_present(
+ name,
+ namespace='default',
+ data=None,
+ source='',
+ template='',
+ **kwargs):
+ '''
+ Ensures that the named configmap is present inside of the specified namespace
+ with the given data.
+ If the configmap exists it will be replaced.
+
+ name
+ The name of the configmap.
+
+ namespace
+ The namespace holding the configmap. The 'default' one is going to be
+ used unless a different one is specified.
+
+ data
+ The dictionary holding the configmaps.
+
+ source
+ A file containing the data of the configmap in plain format.
+
+ template
+ Template engine to be used to render the source file.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ if data and source:
+ return _error(
+ ret,
+ '\'source\' cannot be used in combination with \'data\''
+ )
+
+ configmap = __salt__['kubernetes.show_configmap'](name, namespace, **kwargs)
+
+ if configmap is None:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The configmap is going to be created'
+ return ret
+ res = __salt__['kubernetes.create_configmap'](name=name,
+ namespace=namespace,
+ data=data,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+ ret['changes']['{0}.{1}'.format(namespace, name)] = {
+ 'old': {},
+ 'new': res}
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ # TODO: improve checks # pylint: disable=fixme
+ log.info('Forcing the recreation of the service')
+ ret['comment'] = 'The configmap is already present. Forcing recreation'
+ res = __salt__['kubernetes.replace_configmap'](
+ name=name,
+ namespace=namespace,
+ data=data,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+
+ ret['changes'] = {
+ 'data': res['data']
+ }
+ ret['result'] = True
+ return ret
+
+
+def pod_absent(name, namespace='default', **kwargs):
+ '''
+ Ensures that the named pod is absent from the given namespace.
+
+ name
+ The name of the pod
+
+ namespace
+ The name of the namespace
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ pod = __salt__['kubernetes.show_pod'](name, namespace, **kwargs)
+
+ if pod is None:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The pod does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The pod is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ res = __salt__['kubernetes.delete_pod'](name, namespace, **kwargs)
+ if res['code'] == 200 or res['code'] is None:
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.pod': {
+ 'new': 'absent', 'old': 'present'}}
+ if res['code'] is None:
+ ret['comment'] = 'In progress'
+ else:
+ ret['comment'] = res['message']
+ else:
+ ret['comment'] = 'Something went wrong, response: {0}'.format(res)
+
+ return ret
+
+
+def pod_present(
+ name,
+ namespace='default',
+ metadata=None,
+ spec=None,
+ source='',
+ template='',
+ **kwargs):
+ '''
+ Ensures that the named pod is present inside of the specified
+ namespace with the given metadata and spec.
+ If the pod exists it will be replaced.
+
+ name
+ The name of the pod.
+
+ namespace
+ The namespace holding the pod. The 'default' one is going to be
+ used unless a different one is specified.
+
+ metadata
+ The metadata of the pod object.
+
+ spec
+ The spec of the pod object.
+
+ source
+ A file containing the definition of the pod (metadata and
+ spec) in the official kubernetes format.
+
+ template
+ Template engine to be used to render the source file.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ if (metadata or spec) and source:
+ return _error(
+ ret,
+ '\'source\' cannot be used in combination with \'metadata\' or '
+ '\'spec\''
+ )
+
+ if metadata is None:
+ metadata = {}
+
+ if spec is None:
+ spec = {}
+
+ pod = __salt__['kubernetes.show_pod'](name, namespace, **kwargs)
+
+ if pod is None:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The pod is going to be created'
+ return ret
+ res = __salt__['kubernetes.create_pod'](name=name,
+ namespace=namespace,
+ metadata=metadata,
+ spec=spec,
+ source=source,
+ template=template,
+ saltenv=__env__,
+ **kwargs)
+ ret['changes']['{0}.{1}'.format(namespace, name)] = {
+ 'old': {},
+ 'new': res}
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ # TODO: fix replace_namespaced_pod validation issues
+ ret['comment'] = 'salt is currently unable to replace a pod without ' \
+ 'deleting it. Please perform the removal of the pod requiring ' \
+ 'the \'pod_absent\' state if this is the desired behaviour.'
+ ret['result'] = False
+ return ret
+
+ ret['changes'] = {
+ 'metadata': metadata,
+ 'spec': spec
+ }
+ ret['result'] = True
+ return ret
+
+
+def node_label_absent(name, node, **kwargs):
+ '''
+ Ensures that the named label is absent from the node.
+
+ name
+ The name of the label
+
+ node
+ The name of the node
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ labels = __salt__['kubernetes.node_labels'](node, **kwargs)
+
+ if name not in labels:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The label does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The label is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ __salt__['kubernetes.node_remove_label'](
+ node_name=node,
+ label_name=name,
+ **kwargs)
+
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.node_label': {
+ 'new': 'absent', 'old': 'present'}}
+ ret['comment'] = 'Label removed from node'
+
+ return ret
+
+
+def node_label_folder_absent(name, node, **kwargs):
+ '''
+ Ensures the label folder doesn't exist on the specified node.
+
+ name
+ The name of label folder
+
+ node
+ The name of the node
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+ labels = __salt__['kubernetes.node_labels'](node, **kwargs)
+
+ folder = name.strip("/") + "/"
+ labels_to_drop = []
+ new_labels = []
+ for label in labels:
+ if label.startswith(folder):
+ labels_to_drop.append(label)
+ else:
+ new_labels.append(label)
+
+ if not labels_to_drop:
+ ret['result'] = True if not __opts__['test'] else None
+ ret['comment'] = 'The label folder does not exist'
+ return ret
+
+ if __opts__['test']:
+ ret['comment'] = 'The label folder is going to be deleted'
+ ret['result'] = None
+ return ret
+
+ for label in labels_to_drop:
+ __salt__['kubernetes.node_remove_label'](
+ node_name=node,
+ label_name=label,
+ **kwargs)
+
+ ret['result'] = True
+ ret['changes'] = {
+ 'kubernetes.node_label_folder_absent': {
+ 'new': new_labels, 'old': labels.keys()}}
+ ret['comment'] = 'Label folder removed from node'
+
+ return ret
+
+
+def node_label_present(
+ name,
+ node,
+ value,
+ **kwargs):
+ '''
+ Ensures that the named label is set on the named node
+ with the given value.
+ If the label exists it will be replaced.
+
+ name
+ The name of the label.
+
+ value
+ Value of the label.
+
+ node
+ Node to change.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': False,
+ 'comment': ''}
+
+ labels = __salt__['kubernetes.node_labels'](node, **kwargs)
+
+ if name not in labels:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'The label is going to be set'
+ return ret
+ __salt__['kubernetes.node_add_label'](label_name=name,
+ label_value=value,
+ node_name=node,
+ **kwargs)
+ elif labels[name] == value:
+ ret['result'] = True
+ ret['comment'] = 'The label is already set and has the specified value'
+ return ret
+ else:
+ if __opts__['test']:
+ ret['result'] = None
+ return ret
+
+ ret['comment'] = 'The label is already set, changing the value'
+ __salt__['kubernetes.node_add_label'](
+ node_name=node,
+ label_name=name,
+ label_value=value,
+ **kwargs)
+
+ old_labels = copy.copy(labels)
+ labels[name] = value
+
+ ret['changes']['{0}.{1}'.format(node, name)] = {
+ 'old': old_labels,
+ 'new': labels}
+ ret['result'] = True
+
+ return ret
diff --git a/tests/unit/modules/test_kubernetes.py b/tests/unit/modules/test_kubernetes.py
new file mode 100644
index 0000000000..3357cad2df
--- /dev/null
+++ b/tests/unit/modules/test_kubernetes.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+'''
+ :codeauthor: :email:`Jochen Breuer <jbreuer@suse.de>`
+'''
+
+# Import Python Libs
+from __future__ import absolute_import
+
+# Import Salt Testing Libs
+from salttesting import TestCase, skipIf
+from salttesting.mock import (
+ Mock,
+ patch,
+ NO_MOCK,
+ NO_MOCK_REASON
+)
+
+try:
+ from salt.modules import kubernetes
+except ImportError:
+ kubernetes = False
+
+# Globals
+kubernetes.__salt__ = dict()
+kubernetes.__grains__ = dict()
+kubernetes.__context__ = dict()
+
+
+@skipIf(NO_MOCK, NO_MOCK_REASON)
+@skipIf(kubernetes is False, "Probably Kubernetes client lib is not installed. \
+ Skipping test_kubernetes.py")
+class KubernetesTestCase(TestCase):
+ '''
+ Test cases for salt.modules.kubernetes
+ '''
+
+ def test_nodes(self):
+ '''
+ Test node listing.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.CoreV1Api.return_value = Mock(
+ **{"list_node.return_value.to_dict.return_value":
+ {'items': [{'metadata': {'name': 'mock_node_name'}}]}}
+ )
+ self.assertEqual(kubernetes.nodes(), ['mock_node_name'])
+ self.assertTrue(kubernetes.kubernetes.client.CoreV1Api().list_node().to_dict.called)
+
+ def test_deployments(self):
+ '''
+ Tests deployment listing.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock(
+ **{"list_namespaced_deployment.return_value.to_dict.return_value":
+ {'items': [{'metadata': {'name': 'mock_deployment_name'}}]}}
+ )
+ self.assertEqual(kubernetes.deployments(), ['mock_deployment_name'])
+ self.assertTrue(
+ kubernetes.kubernetes.client.ExtensionsV1beta1Api().list_namespaced_deployment().to_dict.called)
+
+ def test_services(self):
+ '''
+ Tests services listing.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.CoreV1Api.return_value = Mock(
+ **{"list_namespaced_service.return_value.to_dict.return_value":
+ {'items': [{'metadata': {'name': 'mock_service_name'}}]}}
+ )
+ self.assertEqual(kubernetes.services(), ['mock_service_name'])
+ self.assertTrue(kubernetes.kubernetes.client.CoreV1Api().list_namespaced_service().to_dict.called)
+
+ def test_pods(self):
+ '''
+ Tests pods listing.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.CoreV1Api.return_value = Mock(
+ **{"list_namespaced_pod.return_value.to_dict.return_value":
+ {'items': [{'metadata': {'name': 'mock_pod_name'}}]}}
+ )
+ self.assertEqual(kubernetes.pods(), ['mock_pod_name'])
+ self.assertTrue(kubernetes.kubernetes.client.CoreV1Api().
+ list_namespaced_pod().to_dict.called)
+
+ def test_delete_deployments(self):
+ '''
+ Tests deployment creation.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.V1DeleteOptions = Mock(return_value="")
+ mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock(
+ **{"delete_namespaced_deployment.return_value.to_dict.return_value": {}}
+ )
+ self.assertEqual(kubernetes.delete_deployment("test"), {})
+ self.assertTrue(
+ kubernetes.kubernetes.client.ExtensionsV1beta1Api().
+ delete_namespaced_deployment().to_dict.called)
+
+ def test_create_deployments(self):
+ '''
+ Tests deployment creation.
+ :return:
+ '''
+ with patch('salt.modules.kubernetes.kubernetes') as mock_kubernetes_lib:
+ with patch.dict(kubernetes.__salt__, {'config.option': Mock(return_value="")}):
+ mock_kubernetes_lib.client.ExtensionsV1beta1Api.return_value = Mock(
+ **{"create_namespaced_deployment.return_value.to_dict.return_value": {}}
+ )
+ self.assertEqual(kubernetes.create_deployment("test", "default", {}, {},
+ None, None, None), {})
+ self.assertTrue(
+ kubernetes.kubernetes.client.ExtensionsV1beta1Api().
+ create_namespaced_deployment().to_dict.called)
--
2.13.6