Merge pull request #2229 from wfhartford/kubernetes-improvment

Improve the Kubernetes Destination plugin
This commit is contained in:
Curtis 2018-12-21 13:00:46 -08:00 committed by GitHub
commit d60b0c8805
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 147 additions and 42 deletions

View File

@ -25,7 +25,6 @@ DEFAULT_API_VERSION = 'v1'
def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data):
# _resolve_uri(k8s_base_uri, namespace, kind, name, api_ver=DEFAULT_API_VERSION) # _resolve_uri(k8s_base_uri, namespace, kind, name, api_ver=DEFAULT_API_VERSION)
url = _resolve_uri(k8s_base_uri, namespace, kind) url = _resolve_uri(k8s_base_uri, namespace, kind)
current_app.logger.debug("K8S POST request URL: %s", url) current_app.logger.debug("K8S POST request URL: %s", url)
@ -50,11 +49,12 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data):
return return
def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,): def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION):
api_group = 'api' api_group = 'api'
if '/' in api_ver: if '/' in api_ver:
api_group = 'apis' api_group = 'apis'
return '{base}/{api_group}/{api_ver}/namespaces'.format(base=k8s_base_uri, api_group=api_group, api_ver=api_ver) + ('/' + namespace if namespace else '') return '{base}/{api_group}/{api_ver}/namespaces'.format(base=k8s_base_uri, api_group=api_group, api_ver=api_ver) + (
'/' + namespace if namespace else '')
def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_VERSION): def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_VERSION):
@ -74,6 +74,35 @@ def base64encode(string):
return base64.b64encode(string.encode()).decode() return base64.b64encode(string.encode()).decode()
def build_secret(secret_format, secret_name, body, private_key, cert_chain):
secret = {
'apiVersion': 'v1',
'kind': 'Secret',
'type': 'Opaque',
'metadata': {
'name': secret_name,
}
}
if secret_format == 'Full':
secret['data'] = {
'combined.pem': base64encode('%s\n%s' % (body, private_key)),
'ca.crt': base64encode(cert_chain),
'service.key': base64encode(private_key),
'service.crt': base64encode(body),
}
if secret_format == 'TLS':
secret['type'] = 'kubernetes.io/tls'
secret['data'] = {
'tls.crt': base64encode(cert_chain),
'tls.key': base64encode(private_key)
}
if secret_format == 'Certificate':
secret['data'] = {
'tls.crt': base64encode(cert_chain),
}
return secret
class KubernetesDestinationPlugin(DestinationPlugin): class KubernetesDestinationPlugin(DestinationPlugin):
title = 'Kubernetes' title = 'Kubernetes'
slug = 'kubernetes-destination' slug = 'kubernetes-destination'
@ -83,35 +112,81 @@ class KubernetesDestinationPlugin(DestinationPlugin):
author_url = 'https://github.com/mik373/lemur' author_url = 'https://github.com/mik373/lemur'
options = [ options = [
{
'name': 'secretNameFormat',
'type': 'str',
'required': False,
# Validation is difficult. This regex is used by kubectl to validate secret names:
# [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
# Allowing the insertion of "{common_name}" (or any other such placeholder}
# at any point in the string proved very challenging and had a tendency to
# cause my browser to hang. The specified expression will allow any valid string
# but will also accept many invalid strings.
'validation': '(?:[a-z0-9.-]|\\{common_name\\})+',
'helpMessage': 'Must be a valid secret name, possibly including "{common_name}"',
'default': '{common_name}'
},
{ {
'name': 'kubernetesURL', 'name': 'kubernetesURL',
'type': 'str', 'type': 'str',
'required': True, 'required': False,
'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?', 'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?',
'helpMessage': 'Must be a valid Kubernetes server URL!', 'helpMessage': 'Must be a valid Kubernetes server URL!',
'default': 'https://kubernetes.default'
}, },
{ {
'name': 'kubernetesAuthToken', 'name': 'kubernetesAuthToken',
'type': 'str', 'type': 'str',
'required': True, 'required': False,
'validation': '[0-9a-zA-Z-_.]+', 'validation': '[0-9a-zA-Z-_.]+',
'helpMessage': 'Must be a valid Kubernetes server Token!', 'helpMessage': 'Must be a valid Kubernetes server Token!',
}, },
{
'name': 'kubernetesAuthTokenFile',
'type': 'str',
'required': False,
'validation': '(/[^/]+)+',
'helpMessage': 'Must be a valid file path!',
'default': '/var/run/secrets/kubernetes.io/serviceaccount/token'
},
{ {
'name': 'kubernetesServerCertificate', 'name': 'kubernetesServerCertificate',
'type': 'textarea', 'type': 'textarea',
'required': True, 'required': False,
'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END CERTIFICATE-----', 'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END CERTIFICATE-----',
'helpMessage': 'Must be a valid Kubernetes server Certificate!', 'helpMessage': 'Must be a valid Kubernetes server Certificate!',
}, },
{
'name': 'kubernetesServerCertificateFile',
'type': 'str',
'required': False,
'validation': '(/[^/]+)+',
'helpMessage': 'Must be a valid file path!',
'default': '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
},
{ {
'name': 'kubernetesNamespace', 'name': 'kubernetesNamespace',
'type': 'str', 'type': 'str',
'required': True, 'required': False,
'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?', 'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?',
'helpMessage': 'Must be a valid Kubernetes Namespace!', 'helpMessage': 'Must be a valid Kubernetes Namespace!',
}, },
{
'name': 'kubernetesNamespaceFile',
'type': 'str',
'required': False,
'validation': '(/[^/]+)+',
'helpMessage': 'Must be a valid file path!',
'default': '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
},
{
'name': 'secretFormat',
'type': 'select',
'required': True,
'available': ['Full', 'TLS', 'Certificate'],
'helpMessage': 'The type of Secret to create.',
'default': 'Full'
}
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -120,60 +195,90 @@ class KubernetesDestinationPlugin(DestinationPlugin):
def upload(self, name, body, private_key, cert_chain, options, **kwargs): def upload(self, name, body, private_key, cert_chain, options, **kwargs):
try: try:
k8_bearer = self.get_option('kubernetesAuthToken', options)
k8_cert = self.get_option('kubernetesServerCertificate', options)
k8_namespace = self.get_option('kubernetesNamespace', options)
k8_base_uri = self.get_option('kubernetesURL', options) k8_base_uri = self.get_option('kubernetesURL', options)
secret_format = self.get_option('secretFormat', options)
k8s_api = K8sSession(k8_bearer, k8_cert) k8s_api = K8sSession(
self.k8s_bearer(options),
self.k8s_cert(options)
)
cn = common_name(parse_certificate(body)) cn = common_name(parse_certificate(body))
secret_name_format = self.get_option('secretNameFormat', options)
secret_name = secret_name_format.format(common_name=cn)
secret = build_secret(secret_format, secret_name, body, private_key, cert_chain)
err = ensure_resource(
k8s_api,
k8s_base_uri=k8_base_uri,
namespace=self.k8s_namespace(options),
kind="secret",
name=secret_name,
data=secret
)
# in the future once runtime properties can be passed-in - use passed-in secret name
secret_name = 'certs-' + cn
err = ensure_resource(k8s_api, k8s_base_uri=k8_base_uri, namespace=k8_namespace, kind="secret", name=secret_name, data={
'apiVersion': 'v1',
'kind': 'Secret',
'metadata': {
'name': secret_name,
},
'data': {
'combined.pem': base64encode('%s\n%s' % (body, private_key)),
'ca.crt': base64encode(cert_chain),
'service.key': base64encode(private_key),
'service.crt': base64encode(body),
}
})
except Exception as e: except Exception as e:
current_app.logger.exception("Exception in upload: {}".format(e), exc_info=True) current_app.logger.exception("Exception in upload: {}".format(e), exc_info=True)
raise raise
if err is not None: if err is not None:
current_app.logger.debug("Error deploying resource: %s", err) current_app.logger.error("Error deploying resource: %s", err)
raise Exception("Error uploading secret: " + err) raise Exception("Error uploading secret: " + err)
def k8s_bearer(self, options):
bearer = self.get_option('kubernetesAuthToken', options)
if not bearer:
bearer_file = self.get_option('kubernetesAuthTokenFile', options)
with open(bearer_file, "r") as file:
bearer = file.readline()
if bearer:
current_app.logger.debug("Using token read from %s", bearer_file)
else:
raise Exception("Unable to locate token in options or from %s", bearer_file)
else:
current_app.logger.debug("Using token from options")
return bearer
def k8s_cert(self, options):
cert_file = self.get_option('kubernetesServerCertificateFile', options)
cert = self.get_option('kubernetesServerCertificate', options)
if cert:
cert_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'k8.cert')
with open(cert_file, "w") as text_file:
text_file.write(cert)
current_app.logger.debug("Using certificate from options")
else:
current_app.logger.debug("Using certificate from %s", cert_file)
return cert_file
def k8s_namespace(self, options):
namespace = self.get_option('kubernetesNamespace', options)
if not namespace:
namespace_file = self.get_option('kubernetesNamespaceFile', options)
with open(namespace_file, "r") as file:
namespace = file.readline()
if namespace:
current_app.logger.debug("Using namespace %s from %s", namespace, namespace_file)
else:
raise Exception("Unable to locate namespace in options or from %s", namespace_file)
else:
current_app.logger.debug("Using namespace %s from options", namespace)
return namespace
class K8sSession(requests.Session): class K8sSession(requests.Session):
def __init__(self, bearer, cert): def __init__(self, bearer, cert_file):
super(K8sSession, self).__init__() super(K8sSession, self).__init__()
self.headers.update({ self.headers.update({
'Authorization': 'Bearer %s' % bearer 'Authorization': 'Bearer %s' % bearer
}) })
k8_ca = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'k8.cert') self.verify = cert_file
with open(k8_ca, "w") as text_file: def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None,
text_file.write(cert) timeout=30, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None,
json=None):
self.verify = k8_ca
def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=30, allow_redirects=True, proxies=None,
hooks=None, stream=None, verify=None, cert=None, json=None):
""" """
This method overrides the default timeout to be 10s. This method overrides the default timeout to be 10s.
""" """
return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout,
verify, cert, json) allow_redirects, proxies, hooks, stream, verify, cert, json)