mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 09:23:56 +00:00
commit
0bde9f0aef
@ -6,7 +6,6 @@ Salt module to manage Kubernetes cluster
|
|||||||
|
|
||||||
Roadmap:
|
Roadmap:
|
||||||
|
|
||||||
* Remove python-requests dependency
|
|
||||||
* Add creation of K8S objects (pod, rc, service, ...)
|
* Add creation of K8S objects (pod, rc, service, ...)
|
||||||
* Add replace of K8S objects (pod, rc, service, ...)
|
* Add replace of K8S objects (pod, rc, service, ...)
|
||||||
* Add deletion of K8S objects (pod, rc, service, ...)
|
* Add deletion of K8S objects (pod, rc, service, ...)
|
||||||
@ -19,25 +18,25 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import logging as logger
|
||||||
|
import base64
|
||||||
|
import salt.ext.six as six
|
||||||
|
from salt.ext.six.moves.urllib.parse import urlparse as _urlparse # pylint: disable=no-name-in-module
|
||||||
|
|
||||||
# TODO Remove requests dependency
|
# TODO Remove requests dependency
|
||||||
# Import third party libs
|
|
||||||
try:
|
|
||||||
import requests
|
|
||||||
HAS_REQUESTS = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_REQUESTS = False
|
|
||||||
|
|
||||||
import salt.utils
|
import salt.utils
|
||||||
|
import salt.utils.http as http
|
||||||
|
|
||||||
__virtualname__ = 'k8s'
|
__virtualname__ = 'k8s'
|
||||||
|
|
||||||
|
# Setup the logger
|
||||||
|
log = logger.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def __virtual__():
|
def __virtual__():
|
||||||
'''Load load if python-requests is installed.'''
|
'''Load load if python-requests is installed.'''
|
||||||
if not HAS_REQUESTS:
|
|
||||||
return False
|
|
||||||
return __virtualname__
|
return __virtualname__
|
||||||
|
|
||||||
|
|
||||||
@ -45,21 +44,126 @@ def _guess_apiserver(apiserver_url=None):
|
|||||||
'''Try to guees the kubemaster url from environ,
|
'''Try to guees the kubemaster url from environ,
|
||||||
then from `/etc/kubernetes/config` file
|
then from `/etc/kubernetes/config` file
|
||||||
'''
|
'''
|
||||||
|
default_config = "/etc/kubernetes/config"
|
||||||
if apiserver_url is not None:
|
if apiserver_url is not None:
|
||||||
return apiserver_url
|
return apiserver_url
|
||||||
if "KUBERNETES_MASTER" in os.environ:
|
if "KUBERNETES_MASTER" in os.environ:
|
||||||
apiserver_url = os.environ.get("KUBERNETES_MASTER")
|
apiserver_url = os.environ.get("KUBERNETES_MASTER")
|
||||||
else:
|
elif __salt__['config.get']('k8s:master'):
|
||||||
|
apiserver_url = __salt__['config.get']('k8s:master')
|
||||||
|
elif os.path.exists(default_config) or __salt__['config.get']('k8s:config', ""):
|
||||||
|
config = __salt__['config.get']('k8s:config', default_config)
|
||||||
kubeapi_regex = re.compile("""KUBE_MASTER=['"]--master=(.*)['"]""",
|
kubeapi_regex = re.compile("""KUBE_MASTER=['"]--master=(.*)['"]""",
|
||||||
re.MULTILINE)
|
re.MULTILINE)
|
||||||
with salt.utils.fopen("/etc/kubernetes/config") as fh_k8s:
|
with salt.utils.fopen(config) as fh_k8s:
|
||||||
for line in fh_k8s.readlines():
|
for line in fh_k8s.readlines():
|
||||||
match_line = kubeapi_regex.match(line)
|
match_line = kubeapi_regex.match(line)
|
||||||
if match_line:
|
if match_line:
|
||||||
apiserver_url = match_line.group(1)
|
apiserver_url = match_line.group(1)
|
||||||
|
else:
|
||||||
|
# we failed to discover, lets use k8s default address
|
||||||
|
apiserver_url = "http://127.0.0.1:8080"
|
||||||
|
log.debug("Discoverd k8s API server address: {0}".format(apiserver_url))
|
||||||
return apiserver_url
|
return apiserver_url
|
||||||
|
|
||||||
|
|
||||||
|
def _kpost(url, data):
|
||||||
|
''' create any object in kubernetes based on URL '''
|
||||||
|
|
||||||
|
# Prepare headers
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
# Make request
|
||||||
|
log.trace("url is: {0}, data is: {1}".format(url, data))
|
||||||
|
ret = http.query(url, method='POST', header_dict=headers, data=json.dumps(data))
|
||||||
|
# Check requests status
|
||||||
|
if ret.get('error'):
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
return json.loads(ret.get('body'))
|
||||||
|
|
||||||
|
|
||||||
|
def _kput(url, data):
|
||||||
|
''' put any object in kubernetes based on URL '''
|
||||||
|
|
||||||
|
# Prepare headers
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
# Make request
|
||||||
|
ret = http.query(url, method='PUT', header_dict=headers, data=json.dumps(data))
|
||||||
|
# Check requests status
|
||||||
|
if ret.get('error'):
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
return json.loads(ret.get('body'))
|
||||||
|
|
||||||
|
|
||||||
|
def _kpatch(url, data):
|
||||||
|
''' patch any object in kubernetes based on URL '''
|
||||||
|
|
||||||
|
# Prepare headers
|
||||||
|
headers = {"Content-Type": "application/json-patch+json"}
|
||||||
|
# Make request
|
||||||
|
ret = http.query(url, method='PATCH', header_dict=headers,
|
||||||
|
data=json.dumps(data))
|
||||||
|
# Check requests status
|
||||||
|
if ret.get('error'):
|
||||||
|
log.error("Got an error: {0}".format(ret.get("error")))
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
return json.loads(ret.get('body'))
|
||||||
|
|
||||||
|
|
||||||
|
def _kname(obj):
|
||||||
|
'''Get name or names out of json result from API server'''
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return [obj.get("metadata", {}).get("name", "")]
|
||||||
|
elif isinstance(obj, (list, tuple)):
|
||||||
|
names = []
|
||||||
|
for i in obj:
|
||||||
|
names.append(i.get("metadata", {}).get("name", ""))
|
||||||
|
return names
|
||||||
|
else:
|
||||||
|
return "Unknown type"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dns_subdomain(name):
|
||||||
|
''' Check that name is DNS subdomain: One or more lowercase rfc1035/rfc1123
|
||||||
|
labels separated by '.' with a maximum length of 253 characters '''
|
||||||
|
|
||||||
|
dns_subdomain = re.compile(r"""^[a-z0-9\.-]{1,253}$""")
|
||||||
|
if dns_subdomain.match(name):
|
||||||
|
log.debug("Name: {0} is valid DNS subdomain".format(name))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.debug("Name: {0} is not valid DNS subdomain".format(name))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_port_name(name):
|
||||||
|
''' Check that name is IANA service: An alphanumeric (a-z, and 0-9) string,
|
||||||
|
with a maximum length of 15 characters, with the '-' character allowed
|
||||||
|
anywhere except the first or the last character or adjacent to another '-'
|
||||||
|
character, it must contain at least a (a-z) character '''
|
||||||
|
|
||||||
|
port_name = re.compile("""^[a-z0-9]{1,15}$""")
|
||||||
|
if port_name.match(name):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dns_label(name):
|
||||||
|
''' Check that name is DNS label: An alphanumeric (a-z, and 0-9) string,
|
||||||
|
with a maximum length of 63 characters, with the '-' character allowed
|
||||||
|
anywhere except the first or last character, suitable for use as a hostname
|
||||||
|
or segment in a domain name '''
|
||||||
|
|
||||||
|
dns_label = re.compile(r"""^[a-z0-9][a-z0-9\.-]{1,62}$""")
|
||||||
|
if dns_label.match(name):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _guess_node_id(node):
|
def _guess_node_id(node):
|
||||||
'''Try to guess kube node ID using salt minion ID'''
|
'''Try to guess kube node ID using salt minion ID'''
|
||||||
if node is None:
|
if node is None:
|
||||||
@ -70,44 +174,36 @@ def _guess_node_id(node):
|
|||||||
def _get_labels(node, apiserver_url):
|
def _get_labels(node, apiserver_url):
|
||||||
'''Get all labels from a kube node.'''
|
'''Get all labels from a kube node.'''
|
||||||
# Prepare URL
|
# Prepare URL
|
||||||
url = apiserver_url + "/api/v1/nodes/" + node
|
url = "{0}/api/v1/nodes/{1}".format(apiserver_url, node)
|
||||||
# Make request
|
# Make request
|
||||||
ret = requests.get(url)
|
ret = http.query(url)
|
||||||
# Check requests status
|
# Check requests status
|
||||||
try:
|
if 'body' in ret:
|
||||||
ret.raise_for_status()
|
ret = json.loads(ret.get('body'))
|
||||||
except requests.HTTPError as exp:
|
elif ret.get('status', 0) == 404:
|
||||||
if ret.status_code == 404:
|
return "Node {0} doesn't exist".format(node)
|
||||||
return "Node {0} doesn't exist".format(node)
|
else:
|
||||||
else:
|
return ret
|
||||||
return exp
|
|
||||||
# Get and return labels
|
# Get and return labels
|
||||||
return ret.json().get('metadata', {}).get('labels', {})
|
return ret.get('metadata', {}).get('labels', {})
|
||||||
|
|
||||||
|
|
||||||
def _set_labels(node, apiserver_url, labels):
|
def _set_labels(node, apiserver_url, labels):
|
||||||
'''Replace labels dict by a new one'''
|
'''Replace labels dict by a new one'''
|
||||||
# Prepare URL
|
# Prepare URL
|
||||||
url = apiserver_url + "/api/v1/nodes/" + node
|
url = "{0}/api/v1/nodes/{1}".format(apiserver_url, node)
|
||||||
# Prepare data
|
# Prepare data
|
||||||
data = [{"op": "replace", "path": "/metadata/labels", "value": labels}]
|
data = [{"op": "replace", "path": "/metadata/labels", "value": labels}]
|
||||||
# Prepare headers
|
|
||||||
headers = {"Content-Type": "application/json-patch+json"}
|
|
||||||
# Make request
|
# Make request
|
||||||
ret = requests.patch(url, headers=headers, json=data)
|
ret = _kpatch(url, data)
|
||||||
# Check requests status
|
if ret.get("status") == 404:
|
||||||
try:
|
return "Node {0} doesn't exist".format(node)
|
||||||
ret.raise_for_status()
|
|
||||||
except requests.HTTPError as exp:
|
|
||||||
if ret.status_code == 404:
|
|
||||||
return "Node {0} doesn't exist".format(node)
|
|
||||||
else:
|
|
||||||
return exp
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_labels(node=None, apiserver_url=None):
|
def get_labels(node=None, apiserver_url=None):
|
||||||
'''
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
Get labels from the current node
|
Get labels from the current node
|
||||||
|
|
||||||
CLI Example:
|
CLI Example:
|
||||||
@ -127,15 +223,14 @@ def get_labels(node=None, apiserver_url=None):
|
|||||||
|
|
||||||
# Get data
|
# Get data
|
||||||
ret = _get_labels(node, apiserver_url)
|
ret = _get_labels(node, apiserver_url)
|
||||||
|
|
||||||
return {"labels": ret}
|
return {"labels": ret}
|
||||||
|
|
||||||
|
|
||||||
def label_present(
|
def label_present(name, value, node=None, apiserver_url=None):
|
||||||
name,
|
|
||||||
value,
|
|
||||||
node=None,
|
|
||||||
apiserver_url=None):
|
|
||||||
'''
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
Set label to the current node
|
Set label to the current node
|
||||||
|
|
||||||
CLI Example:
|
CLI Example:
|
||||||
@ -163,14 +258,26 @@ def label_present(
|
|||||||
# This is a new label
|
# This is a new label
|
||||||
ret['changes'] = {name: value}
|
ret['changes'] = {name: value}
|
||||||
labels[name] = str(value)
|
labels[name] = str(value)
|
||||||
_set_labels(node, apiserver_url, labels)
|
res = _set_labels(node, apiserver_url, labels)
|
||||||
ret['comment'] = "Label {0} created".format(name)
|
if res.get('status') == 409:
|
||||||
|
# there is an update during operation, need to retry
|
||||||
|
log.debug("Got 409, will try later")
|
||||||
|
ret['changes'] = {}
|
||||||
|
ret['comment'] = "Could not create label {0}, please retry".format(name)
|
||||||
|
else:
|
||||||
|
ret['comment'] = "Label {0} created".format(name)
|
||||||
elif labels.get(name) != str(value):
|
elif labels.get(name) != str(value):
|
||||||
# This is a old label and we are going to edit it
|
# This is a old label and we are going to edit it
|
||||||
ret['changes'] = {name: str(value)}
|
ret['changes'] = {name: str(value)}
|
||||||
labels[name] = value
|
labels[name] = str(value)
|
||||||
_set_labels(node, apiserver_url, labels)
|
res = _set_labels(node, apiserver_url, labels)
|
||||||
ret['comment'] = "Label {0} updated".format(name)
|
if res.get('status') == 409:
|
||||||
|
# there is an update during operation, need to retry
|
||||||
|
log.debug("Got 409, will try later")
|
||||||
|
ret['changes'] = {}
|
||||||
|
ret['comment'] = "Could not update label {0}, please retry".format(name)
|
||||||
|
else:
|
||||||
|
ret['comment'] = "Label {0} updated".format(name)
|
||||||
else:
|
else:
|
||||||
# This is a old label and it has already the wanted value
|
# This is a old label and it has already the wanted value
|
||||||
ret['comment'] = "Label {0} already set".format(name)
|
ret['comment'] = "Label {0} already set".format(name)
|
||||||
@ -178,11 +285,10 @@ def label_present(
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def label_absent(
|
def label_absent(name, node=None, apiserver_url=None):
|
||||||
name,
|
|
||||||
node=None,
|
|
||||||
apiserver_url=None):
|
|
||||||
'''
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
Delete label to the current node
|
Delete label to the current node
|
||||||
|
|
||||||
CLI Example:
|
CLI Example:
|
||||||
@ -213,18 +319,23 @@ def label_absent(
|
|||||||
ret['comment'] = "Label {0} already absent".format(name)
|
ret['comment'] = "Label {0} already absent".format(name)
|
||||||
else:
|
else:
|
||||||
# Label needs to be delete
|
# Label needs to be delete
|
||||||
_set_labels(node, apiserver_url, labels)
|
res = _set_labels(node, apiserver_url, labels)
|
||||||
ret['changes'] = {"deleted": name}
|
if res.get('status') == 409:
|
||||||
ret['comment'] = "Label {0} absent".format(name)
|
# there is an update during operation, need to retry
|
||||||
|
log.debug("Got 409, will try later")
|
||||||
|
ret['changes'] = {}
|
||||||
|
ret['comment'] = "Could not delete label {0}, please retry".format(name)
|
||||||
|
else:
|
||||||
|
ret['changes'] = {"deleted": name}
|
||||||
|
ret['comment'] = "Label {0} absent".format(name)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def label_folder_absent(
|
def label_folder_absent(name, node=None, apiserver_url=None):
|
||||||
name,
|
|
||||||
node=None,
|
|
||||||
apiserver_url=None):
|
|
||||||
'''
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
Delete label folder to the current node
|
Delete label folder to the current node
|
||||||
|
|
||||||
CLI Example:
|
CLI Example:
|
||||||
@ -256,8 +367,438 @@ def label_folder_absent(
|
|||||||
ret['comment'] = "Label folder {0} already absent".format(folder)
|
ret['comment'] = "Label folder {0} already absent".format(folder)
|
||||||
else:
|
else:
|
||||||
# Label needs to be delete
|
# Label needs to be delete
|
||||||
_set_labels(node, apiserver_url, labels)
|
res = _set_labels(node, apiserver_url, labels)
|
||||||
ret['changes'] = {"deleted": folder}
|
if res.get('status') == 409:
|
||||||
ret['comment'] = "Label folder {0} absent".format(folder)
|
log.debug("Got 409, will try later")
|
||||||
|
ret['changes'] = {}
|
||||||
|
ret['comment'] = "Could not delete label folder {0}, please retry".format(folder)
|
||||||
|
else:
|
||||||
|
ret['changes'] = {"deleted": folder}
|
||||||
|
ret['comment'] = "Label folder {0} absent".format(folder)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# Namespaces
|
||||||
|
def _get_namespaces(apiserver_url, name=""):
|
||||||
|
'''Get namespace is namespace is defined otherwise return all namespaces'''
|
||||||
|
# Prepare URL
|
||||||
|
url = "{0}/api/v1/namespaces/{1}".format(apiserver_url, name)
|
||||||
|
# Make request
|
||||||
|
ret = http.query(url)
|
||||||
|
if ret.get("body"):
|
||||||
|
return json.loads(ret.get("body"))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_namespace(namespace, apiserver_url):
|
||||||
|
''' create namespace on the defined k8s cluster '''
|
||||||
|
# Prepare URL
|
||||||
|
url = "{0}/api/v1/namespaces".format(apiserver_url)
|
||||||
|
# Prepare data
|
||||||
|
data = {
|
||||||
|
"kind": "Namespace",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": {
|
||||||
|
"name": namespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.trace("namespace creation requests: {0}".format(data))
|
||||||
|
# Make request
|
||||||
|
ret = _kpost(url, data)
|
||||||
|
log.trace("result is: {0}".format(ret))
|
||||||
|
# Check requests status
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def create_namespace(name, apiserver_url=None):
|
||||||
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
|
Create kubernetes namespace from the name, similar to the functionality added to kubectl since v.1.2.0:
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
kubectl create namespaces namespace-name
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.create_namespace namespace_name
|
||||||
|
|
||||||
|
salt '*' k8s.create_namespace namespace_name http://kube-master.cluster.local
|
||||||
|
|
||||||
|
'''
|
||||||
|
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
|
||||||
|
|
||||||
|
# Try to get kubernetes master
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
if apiserver_url is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not _get_namespaces(apiserver_url, name):
|
||||||
|
# This is a new namespace
|
||||||
|
_create_namespace(name, apiserver_url)
|
||||||
|
ret['changes'] = name
|
||||||
|
ret['comment'] = "Namespace {0} created".format(name)
|
||||||
|
else:
|
||||||
|
ret['comment'] = "Namespace {0} already present".format(name)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_namespaces(namespace="", apiserver_url=None):
|
||||||
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
|
Get one or all kubernetes namespaces.
|
||||||
|
|
||||||
|
If namespace parameter is omitted, all namespaces will be returned back to user, similar to following kubectl example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
kubectl get namespaces -o json
|
||||||
|
|
||||||
|
In case namespace is set by user, the output will be similar to the one from kubectl:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
kubectl get namespaces namespace_name -o json
|
||||||
|
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.get_namespaces
|
||||||
|
salt '*' k8s.get_namespaces namespace_name http://kube-master.cluster.local
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Try to get kubernetes master
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
if apiserver_url is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get data
|
||||||
|
ret = _get_namespaces(apiserver_url, namespace)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
def _get_secrets(namespace, name, apiserver_url):
|
||||||
|
'''Get secrets of the namespace.'''
|
||||||
|
# Prepare URL
|
||||||
|
url = "{0}/api/v1/namespaces/{1}/secrets/{2}".format(apiserver_url,
|
||||||
|
namespace, name)
|
||||||
|
# Make request
|
||||||
|
ret = http.query(url)
|
||||||
|
if ret.get("body"):
|
||||||
|
return json.loads(ret.get("body"))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _update_secret(namespace, name, data, apiserver_url):
|
||||||
|
'''Replace secrets data by a new one'''
|
||||||
|
# Prepare URL
|
||||||
|
url = "{0}/api/v1/namespaces/{1}/secrets/{2}".format(apiserver_url,
|
||||||
|
namespace, name)
|
||||||
|
# Prepare data
|
||||||
|
data = [{"op": "replace", "path": "/data", "value": data}]
|
||||||
|
# Make request
|
||||||
|
ret = _kpatch(url, data)
|
||||||
|
if ret.get("status") == 404:
|
||||||
|
return "Node {0} doesn't exist".format(url)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _create_secret(namespace, name, data, apiserver_url):
|
||||||
|
''' create namespace on the defined k8s cluster '''
|
||||||
|
# Prepare URL
|
||||||
|
url = "{0}/api/v1/namespaces/{1}/secrets".format(apiserver_url, namespace)
|
||||||
|
# Prepare data
|
||||||
|
request = {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": {
|
||||||
|
"name": name,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
# Make request
|
||||||
|
ret = _kpost(url, request)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_secret_file(filename):
|
||||||
|
if os.path.exists(filename) and os.path.isfile(filename):
|
||||||
|
log.debug("File: {0} is valid secret file".format(filename))
|
||||||
|
return True
|
||||||
|
log.warn("File: {0} does not exists or not file".format(filename))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _file_encode(filename):
|
||||||
|
log.trace("Encoding secret file: {0}".format(filename))
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
return base64.b64encode(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_secrets(secrets):
|
||||||
|
items = secrets.get("items", [])
|
||||||
|
if items:
|
||||||
|
for i, secret in enumerate(items):
|
||||||
|
log.trace(i, secret)
|
||||||
|
for k, v in secret.get("data", {}).iteritems():
|
||||||
|
items[i]['data'][k] = base64.b64decode(v)
|
||||||
|
secrets["items"] = items
|
||||||
|
return secrets
|
||||||
|
else:
|
||||||
|
for k, v in secrets.get("data", {}).iteritems():
|
||||||
|
secrets['data'][k] = base64.b64decode(v)
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
|
||||||
|
def get_secrets(namespace, name="", apiserver_url=None, decode=False, brief=False):
|
||||||
|
'''
|
||||||
|
Get k8s namespaces
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.get_secrets namespace_name
|
||||||
|
salt '*' k8s.get_secrets namespace_name secret_name http://kube-master.cluster.local
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Try to get kubernetes master
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
if apiserver_url is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get data
|
||||||
|
if not decode:
|
||||||
|
ret = _get_secrets(namespace, name, apiserver_url)
|
||||||
|
else:
|
||||||
|
ret = _decode_secrets(_get_secrets(namespace, name, apiserver_url))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _source_encode(source, saltenv):
|
||||||
|
try:
|
||||||
|
source_url = _urlparse(source)
|
||||||
|
except TypeError:
|
||||||
|
return '', {}, ('Invalid format for source parameter')
|
||||||
|
|
||||||
|
protos = ('salt', 'http', 'https', 'ftp', 'swift', 's3', 'file')
|
||||||
|
|
||||||
|
log.trace("parsed source looks like: {0}".format(source_url))
|
||||||
|
if not source_url.scheme or source_url.scheme == 'file':
|
||||||
|
# just a regular file
|
||||||
|
filename = os.path.abspath(source_url.path)
|
||||||
|
sname = os.path.basename(filename)
|
||||||
|
log.debug("Source is a regular local file: {0}".format(source_url.path))
|
||||||
|
if _is_dns_subdomain(sname) and _is_valid_secret_file(filename):
|
||||||
|
return sname, _file_encode(filename)
|
||||||
|
else:
|
||||||
|
if source_url.scheme in protos:
|
||||||
|
# The source is a file on a server
|
||||||
|
filename = __salt__['cp.cache_file'](source, saltenv)
|
||||||
|
if not filename:
|
||||||
|
log.warn("Source file: {0} can not be retrieved".format(source))
|
||||||
|
return "", ""
|
||||||
|
return os.path.basename(filename), _file_encode(filename)
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
|
def update_secret(namespace, name, sources, apiserver_url=None, force=True, saltenv='base'):
|
||||||
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
|
alias to k8s.create_secret with update=true
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.update_secret namespace_name secret_name sources [apiserver_url] [force=true] [update=false] [saltenv='base']
|
||||||
|
|
||||||
|
sources are either dictionary of {name: path, name1: path} pairs or array of strings defining paths.
|
||||||
|
|
||||||
|
Example of paths array:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
['/full/path/filename', "file:///full/path/filename", "salt://secret/storage/file.txt", "http://user:password@securesite.com/secret-file.json"]
|
||||||
|
|
||||||
|
Example of dictionaries:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
{"nameit": '/full/path/fiename', name2: "salt://secret/storage/file.txt"}
|
||||||
|
|
||||||
|
optional parameters accepted:
|
||||||
|
|
||||||
|
force=[true] default value is true
|
||||||
|
if the to False, secret will not be created in case one of the files is not
|
||||||
|
valid kubernetes secret. e.g. capital letters in secret name or _
|
||||||
|
in case force is set to True, wrong files will be skipped but secret will be created any way.
|
||||||
|
|
||||||
|
saltenv=['base'] default value is base
|
||||||
|
in case 'salt://' path is used, this parameter can change the visibility of files
|
||||||
|
|
||||||
|
'''
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
|
||||||
|
ret = create_secret(namespace, name, sources, apiserver_url=apiserver_url,
|
||||||
|
force=force, update=True, saltenv=saltenv)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def create_secret(namespace, name, sources, apiserver_url=None, force=False, update=False, saltenv='base'):
|
||||||
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
|
Create k8s secrets in the defined namespace from the list of files
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.create_secret namespace_name secret_name sources
|
||||||
|
|
||||||
|
salt '*' k8s.create_secret namespace_name secret_name sources
|
||||||
|
http://kube-master.cluster.local
|
||||||
|
|
||||||
|
sources are either dictionary of {name: path, name1: path} pairs or array of strings defining paths.
|
||||||
|
|
||||||
|
Example of paths array:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
['/full/path/filename', "file:///full/path/filename", "salt://secret/storage/file.txt", "http://user:password@securesite.com/secret-file.json"]
|
||||||
|
|
||||||
|
Example of dictionaries:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
{"nameit": '/full/path/fiename', name2: "salt://secret/storage/file.txt"}
|
||||||
|
|
||||||
|
optional parameters accepted:
|
||||||
|
|
||||||
|
update=[false] default value is false
|
||||||
|
if set to false, and secret is already present on the cluster - warning will be returned and no changes to the secret will be done.
|
||||||
|
In case it is set to "true" and secret is present but data is differ - secret will be updated.
|
||||||
|
|
||||||
|
force=[true] default value is true
|
||||||
|
if the to False, secret will not be created in case one of the files is not
|
||||||
|
valid kubernetes secret. e.g. capital letters in secret name or _
|
||||||
|
in case force is set to True, wrong files will be skipped but secret will be created any way.
|
||||||
|
|
||||||
|
saltenv=['base'] default value is base
|
||||||
|
in case 'salt://' path is used, this parameter can change the visibility of files
|
||||||
|
|
||||||
|
'''
|
||||||
|
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
|
||||||
|
|
||||||
|
if not sources:
|
||||||
|
return {'name': name, 'result': False, 'comment': 'No source available', 'changes': {}}
|
||||||
|
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
# we need namespace to create secret in it
|
||||||
|
if not _get_namespaces(apiserver_url, namespace):
|
||||||
|
if force:
|
||||||
|
_create_namespace(namespace, apiserver_url)
|
||||||
|
else:
|
||||||
|
return {'name': name, 'result': False, 'comment': "Namespace doesn't exists", 'changes': {}}
|
||||||
|
|
||||||
|
secret = _get_secrets(namespace, name, apiserver_url)
|
||||||
|
if secret and not update:
|
||||||
|
log.info("Secret {0} is already present on {1}".format(name, namespace))
|
||||||
|
return {'name': name, 'result': False,
|
||||||
|
'comment': 'Secret {0} is already present'.format(name),
|
||||||
|
'changes': {}}
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
log.debug("source is: {0}".format(source))
|
||||||
|
if isinstance(source, dict):
|
||||||
|
# format is array of dictionaries:
|
||||||
|
# [{public_auth: salt://public_key}, {test: "/tmp/test"}]
|
||||||
|
log.trace("source is dictionary: {0}".format(source))
|
||||||
|
for k, v in source.iteritems():
|
||||||
|
sname, encoded = _source_encode(v, saltenv)
|
||||||
|
if sname == encoded == "":
|
||||||
|
ret['comment'] += "Source file {0} is missing or name is incorrect\n".format(v)
|
||||||
|
if force:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
data[k] = encoded
|
||||||
|
elif isinstance(source, six.string_types):
|
||||||
|
# expected format is array of filenames
|
||||||
|
sname, encoded = _source_encode(source, saltenv)
|
||||||
|
if sname == encoded == "":
|
||||||
|
if force:
|
||||||
|
ret['comment'] += "Source file {0} is missing or name is incorrect\n".format(source)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
data[sname] = encoded
|
||||||
|
|
||||||
|
log.trace("secret data is: {0}".format(data))
|
||||||
|
|
||||||
|
if secret and update:
|
||||||
|
if not data:
|
||||||
|
ret["comment"] += "Could not find source files or your sources are empty"
|
||||||
|
ret["result"] = False
|
||||||
|
elif secret.get("data") and data != secret.get("data"):
|
||||||
|
res = _update_secret(namespace, name, data, apiserver_url)
|
||||||
|
ret['comment'] = 'Updated secret'
|
||||||
|
ret['changes'] = 'Updated secret'
|
||||||
|
else:
|
||||||
|
log.debug("Secret has not been changed on cluster, skipping it")
|
||||||
|
ret['comment'] = 'Has not been changed on cluster, skipping it'
|
||||||
|
else:
|
||||||
|
res = _create_secret(namespace, name, data, apiserver_url)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def delete_secret(namespace, name, apiserver_url=None, force=True):
|
||||||
|
'''
|
||||||
|
.. versionadded:: 2016.3.0
|
||||||
|
|
||||||
|
Delete kubernetes secret in the defined namespace. Namespace is the mandatory parameter as well as name.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' k8s.delete_secret namespace_name secret_name
|
||||||
|
|
||||||
|
salt '*' k8s.delete_secret namespace_name secret_name http://kube-master.cluster.local
|
||||||
|
|
||||||
|
'''
|
||||||
|
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
|
||||||
|
|
||||||
|
# Try to get kubernetes master
|
||||||
|
apiserver_url = _guess_apiserver(apiserver_url)
|
||||||
|
if apiserver_url is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# we need namespace to delete secret in it
|
||||||
|
if not _get_namespaces(apiserver_url, namespace):
|
||||||
|
return {'name': name, 'result': False,
|
||||||
|
'comment': "Namespace doesn't exists, can't delete anything there",
|
||||||
|
'changes': {}}
|
||||||
|
|
||||||
|
url = "{0}/api/v1/namespaces/{1}/secrets/{2}".format(apiserver_url,
|
||||||
|
namespace, name)
|
||||||
|
res = http.query(url, method='DELETE')
|
||||||
|
if res.get('body'):
|
||||||
|
ret['comment'] = "Removed secret {0} in {1} namespace".format(name,
|
||||||
|
namespace)
|
||||||
|
return ret
|
||||||
|
400
tests/unit/modules/k8s_test.py
Executable file
400
tests/unit/modules/k8s_test.py
Executable file
@ -0,0 +1,400 @@
|
|||||||
|
# Import pytohn libs
|
||||||
|
from __future__ import absolute_import
|
||||||
|
# Import Salt Testing libs
|
||||||
|
from salttesting import skipIf, TestCase
|
||||||
|
from salttesting.helpers import ensure_in_syspath, skip_if_binaries_missing
|
||||||
|
ensure_in_syspath('../../')
|
||||||
|
# from unittest import TestCase
|
||||||
|
|
||||||
|
# Import Salt execution module to test
|
||||||
|
# from salt.modules import k8s
|
||||||
|
# FIXME
|
||||||
|
import salt.modules.k8s as k8s
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
TestCase.maxDiff = None
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_binaries_missing(['kubectl'])
|
||||||
|
class TestK8SNamespace(TestCase):
|
||||||
|
|
||||||
|
def test_get_namespaces(self):
|
||||||
|
res = k8s.get_namespaces(apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = len(res.get("items"))
|
||||||
|
proc = Popen(["kubectl", "get", "namespaces", "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = len(kubectl_out.get("items"))
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_get_one_namespace(self):
|
||||||
|
res = k8s.get_namespaces("default", apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = res.get("metadata", {}).get("name", "a")
|
||||||
|
proc = Popen(["kubectl", "get", "namespaces", "default", "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = kubectl_out.get("metadata", {}).get("name", "b")
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_create_namespace(self):
|
||||||
|
hash = hashlib.sha1()
|
||||||
|
hash.update(str(time.time()))
|
||||||
|
nsname = hash.hexdigest()[:16]
|
||||||
|
res = k8s.create_namespace(nsname, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
proc = Popen(["kubectl", "get", "namespaces", nsname, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
# if creation is failed, kubernetes return non json error message
|
||||||
|
self.assertTrue(isinstance(kubectl_out, dict))
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_binaries_missing(['kubectl'])
|
||||||
|
class TestK8SSecrets(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
hash = hashlib.sha1()
|
||||||
|
hash.update(str(time.time()))
|
||||||
|
self.name = hash.hexdigest()[:16]
|
||||||
|
data = {"testsecret": base64.encodestring("teststring")}
|
||||||
|
self.request = {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": {
|
||||||
|
"name": self.name,
|
||||||
|
"namespace": "default",
|
||||||
|
},
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_secrets(self):
|
||||||
|
res = k8s.get_secrets("default", apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = len(res.get("items", []))
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "secrets", "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = len(kubectl_out.get("items", []))
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_get_one_secret(self):
|
||||||
|
name = self.name
|
||||||
|
filename = "/tmp/{0}.json".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(self.request, f)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.1)
|
||||||
|
res = k8s.get_secrets("default", name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = res.get("metadata", {}).get("name", "a")
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "secrets", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = kubectl_out.get("metadata", {}).get("name", "b")
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_get_decoded_secret(self):
|
||||||
|
name = self.name
|
||||||
|
filename = "/tmp/{0}.json".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(self.request, f)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give etcd to populate data on all nodes
|
||||||
|
time.sleep(0.1)
|
||||||
|
res = k8s.get_secrets("default", name, apiserver_url="http://127.0.0.1:8080", decode=True)
|
||||||
|
a = res.get("data", {}).get("testsecret", )
|
||||||
|
self.assertEqual(a, "teststring")
|
||||||
|
|
||||||
|
def test_create_secret(self):
|
||||||
|
name = self.name
|
||||||
|
names = []
|
||||||
|
expected_data = {}
|
||||||
|
for i in range(2):
|
||||||
|
names.append("/tmp/{0}-{1}".format(name, i))
|
||||||
|
with open("/tmp/{0}-{1}".format(name, i), 'w') as f:
|
||||||
|
expected_data["{0}-{1}".format(name, i)] = base64.b64encode("{0}{1}".format(name, i))
|
||||||
|
f.write("{0}{1}".format(name, i))
|
||||||
|
res = k8s.create_secret("default", name, names, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "secrets", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
# if creation is failed, kubernetes return non json error message
|
||||||
|
b = kubectl_out.get("data", {})
|
||||||
|
self.assertTrue(isinstance(kubectl_out, dict))
|
||||||
|
self.assertEqual(expected_data, b)
|
||||||
|
|
||||||
|
def test_update_secret(self):
|
||||||
|
name = self.name
|
||||||
|
filename = "/tmp/{0}.json".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(self.request, f)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.1)
|
||||||
|
expected_data = {}
|
||||||
|
names = []
|
||||||
|
for i in range(3):
|
||||||
|
names.append("/tmp/{0}-{1}-updated".format(name, i))
|
||||||
|
with open("/tmp/{0}-{1}-updated".format(name, i), 'w') as f:
|
||||||
|
expected_data["{0}-{1}-updated".format(name, i)] = base64.b64encode("{0}{1}-updated".format(name, i))
|
||||||
|
f.write("{0}{1}-updated".format(name, i))
|
||||||
|
|
||||||
|
res = k8s.update_secret("default", name, names, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
# if creation is failed, kubernetes return non json error message
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "secrets", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
# if creation is failed, kubernetes return non json error message
|
||||||
|
b = kubectl_out.get("data", {})
|
||||||
|
self.assertTrue(isinstance(kubectl_out, dict))
|
||||||
|
self.assertEqual(expected_data, b)
|
||||||
|
|
||||||
|
def test_delete_secret(self):
|
||||||
|
name = self.name
|
||||||
|
filename = "/tmp/{0}.json".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(self.request, f)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.1)
|
||||||
|
res = k8s.delete_secret("default", name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
time.sleep(0.1)
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "secrets", name, "-o", "json"], stdout=PIPE, stderr=PIPE)
|
||||||
|
kubectl_out, err = proc.communicate()
|
||||||
|
# stdout is empty, stderr is showing something like "not found"
|
||||||
|
self.assertEqual('', kubectl_out)
|
||||||
|
self.assertEqual('Error from server: secrets "{0}" not found\n'.format(name), err)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_binaries_missing(['kubectl'])
|
||||||
|
class TestK8SResourceQuotas(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
hash = hashlib.sha1()
|
||||||
|
hash.update(str(time.time()))
|
||||||
|
self.name = hash.hexdigest()[:16]
|
||||||
|
|
||||||
|
def test_get_resource_quotas(self):
|
||||||
|
name = self.name
|
||||||
|
namespace = self.name
|
||||||
|
create_namespace = Popen(["kubectl", "create", "namespace", namespace], stdout=PIPE)
|
||||||
|
create_namespace = Popen(["kubectl", "create", "namespace", namespace], stdout=PIPE)
|
||||||
|
request = """
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ResourceQuota
|
||||||
|
metadata:
|
||||||
|
name: {0}
|
||||||
|
spec:
|
||||||
|
hard:
|
||||||
|
cpu: "20"
|
||||||
|
memory: 1Gi
|
||||||
|
persistentvolumeclaims: "10"
|
||||||
|
pods: "10"
|
||||||
|
replicationcontrollers: "20"
|
||||||
|
resourcequotas: "1"
|
||||||
|
secrets: "10"
|
||||||
|
services: "5"
|
||||||
|
""".format(name)
|
||||||
|
filename = "/tmp/{0}.yaml".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(request)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace={0}".format(namespace), "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.2)
|
||||||
|
res = k8s.get_resource_quotas(namespace, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = len(res.get("items", []))
|
||||||
|
proc = Popen(["kubectl", "--namespace={0}".format(namespace), "get", "quota", "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = len(kubectl_out.get("items", []))
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_get_one_resource_quota(self):
|
||||||
|
name = self.name
|
||||||
|
namespace = self.name
|
||||||
|
create_namespace = Popen(["kubectl", "create", "namespace", namespace], stdout=PIPE)
|
||||||
|
request = """
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ResourceQuota
|
||||||
|
metadata:
|
||||||
|
name: {0}
|
||||||
|
spec:
|
||||||
|
hard:
|
||||||
|
cpu: "20"
|
||||||
|
memory: 1Gi
|
||||||
|
persistentvolumeclaims: "10"
|
||||||
|
pods: "10"
|
||||||
|
replicationcontrollers: "20"
|
||||||
|
resourcequotas: "1"
|
||||||
|
secrets: "10"
|
||||||
|
services: "5"
|
||||||
|
""".format(name)
|
||||||
|
filename = "/tmp/{0}.yaml".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(request)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace={0}".format(namespace), "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.2)
|
||||||
|
res = k8s.get_resource_quotas(namespace, name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = res.get("metadata", {}).get("name", "a")
|
||||||
|
proc = Popen(["kubectl", "--namespace={0}".format(namespace), "get", "quota", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = kubectl_out.get("metadata", {}).get("name", "b")
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_create_resource_quota(self):
|
||||||
|
name = self.name
|
||||||
|
namespace = self.name
|
||||||
|
create_namespace = Popen(["kubectl", "create", "namespace", namespace], stdout=PIPE)
|
||||||
|
quota = {
|
||||||
|
"cpu": "20",
|
||||||
|
"memory": "1Gi"
|
||||||
|
}
|
||||||
|
res = k8s.create_resource_quota(namespace, quota, name=name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
proc = Popen(["kubectl", "--namespace={0}".format(namespace), "get", "quota", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
self.assertTrue(isinstance(kubectl_out, dict))
|
||||||
|
|
||||||
|
def test_update_resource_quota(self):
|
||||||
|
name = self.name
|
||||||
|
namespace = self.name
|
||||||
|
create_namespace = Popen(["kubectl", "create", "namespace", namespace], stdout=PIPE)
|
||||||
|
request = """
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ResourceQuota
|
||||||
|
metadata:
|
||||||
|
name: {0}
|
||||||
|
spec:
|
||||||
|
hard:
|
||||||
|
cpu: "20"
|
||||||
|
memory: 1Gi
|
||||||
|
persistentvolumeclaims: "10"
|
||||||
|
pods: "10"
|
||||||
|
replicationcontrollers: "20"
|
||||||
|
resourcequotas: "1"
|
||||||
|
secrets: "10"
|
||||||
|
services: "5"
|
||||||
|
""".format(name)
|
||||||
|
filename = "/tmp/{0}.yaml".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(request)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace={0}".format(namespace), "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.2)
|
||||||
|
quota = {
|
||||||
|
"cpu": "10",
|
||||||
|
"memory": "2Gi"
|
||||||
|
}
|
||||||
|
res = k8s.create_resource_quota(namespace, quota, name=name, apiserver_url="http://127.0.0.1:8080", update=True)
|
||||||
|
proc = Popen(["kubectl", "--namespace={0}".format(namespace), "get", "quota", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
limit = kubectl_out.get("spec").get("hard").get("memory")
|
||||||
|
self.assertEqual("2Gi", limit)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_if_binaries_missing(['kubectl'])
|
||||||
|
class TestK8SLimitrange(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
hash = hashlib.sha1()
|
||||||
|
hash.update(str(time.time()))
|
||||||
|
self.name = hash.hexdigest()[:16]
|
||||||
|
|
||||||
|
def test_create_limit_range(self):
|
||||||
|
name = self.name
|
||||||
|
limits = {
|
||||||
|
"Container": {
|
||||||
|
"defaultRequest": {
|
||||||
|
"cpu": "100m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = k8s.create_limit_range("default", limits, name=name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "limits", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
self.assertTrue(isinstance(kubectl_out, dict))
|
||||||
|
|
||||||
|
def test_update_limit_range(self):
|
||||||
|
name = self.name
|
||||||
|
request = """
|
||||||
|
apiVersion: v1
|
||||||
|
kind: LimitRange
|
||||||
|
metadata:
|
||||||
|
name: {0}
|
||||||
|
spec:
|
||||||
|
limits:
|
||||||
|
- default:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 512Mi
|
||||||
|
defaultRequest:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
type: Container
|
||||||
|
""".format(name)
|
||||||
|
limits = {
|
||||||
|
"Container": {
|
||||||
|
"defaultRequest": {
|
||||||
|
"cpu": "100m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filename = "/tmp/{0}.yaml".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(request)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.1)
|
||||||
|
res = k8s.create_limit_range("default", limits, name=name, apiserver_url="http://127.0.0.1:8080", update=True)
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "limits", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
limit = kubectl_out.get("spec").get("limits")[0].get("defaultRequest").get("cpu")
|
||||||
|
self.assertEqual("100m", limit)
|
||||||
|
|
||||||
|
def test_get_limit_ranges(self):
|
||||||
|
res = k8s.get_limit_ranges("default", apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = len(res.get("items", []))
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "limits", "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = len(kubectl_out.get("items", []))
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
def test_get_one_limit_range(self):
|
||||||
|
name = self.name
|
||||||
|
request = """
|
||||||
|
apiVersion: v1
|
||||||
|
kind: LimitRange
|
||||||
|
metadata:
|
||||||
|
name: {0}
|
||||||
|
spec:
|
||||||
|
limits:
|
||||||
|
- default:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 512Mi
|
||||||
|
defaultRequest:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
type: Container
|
||||||
|
""".format(name)
|
||||||
|
filename = "/tmp/{0}.yaml".format(name)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(request)
|
||||||
|
|
||||||
|
create = Popen(["kubectl", "--namespace=default", "create", "-f", filename], stdout=PIPE)
|
||||||
|
# wee need to give kubernetes time save data in etcd
|
||||||
|
time.sleep(0.1)
|
||||||
|
res = k8s.get_limit_ranges("default", name, apiserver_url="http://127.0.0.1:8080")
|
||||||
|
a = res.get("metadata", {}).get("name", "a")
|
||||||
|
proc = Popen(["kubectl", "--namespace=default", "get", "limits", name, "-o", "json"], stdout=PIPE)
|
||||||
|
kubectl_out = json.loads(proc.communicate()[0])
|
||||||
|
b = kubectl_out.get("metadata", {}).get("name", "b")
|
||||||
|
self.assertEqual(a, b)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from integration import run_tests
|
||||||
|
run_tests(TestK8SNamespace,
|
||||||
|
TestK8SSecrets,
|
||||||
|
TestK8SResourceQuotas,
|
||||||
|
TestK8SLimitRange,
|
||||||
|
needs_daemon=False)
|
Loading…
Reference in New Issue
Block a user