Merge pull request #2229 from wfhartford/kubernetes-improvment
Improve the Kubernetes Destination plugin
This commit is contained in:
commit
d60b0c8805
|
@ -25,7 +25,6 @@ DEFAULT_API_VERSION = 'v1'
|
|||
|
||||
|
||||
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)
|
||||
url = _resolve_uri(k8s_base_uri, namespace, kind)
|
||||
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
|
||||
|
||||
|
||||
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'
|
||||
if '/' in api_ver:
|
||||
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):
|
||||
|
@ -74,6 +74,35 @@ def base64encode(string):
|
|||
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):
|
||||
title = 'Kubernetes'
|
||||
slug = 'kubernetes-destination'
|
||||
|
@ -83,35 +112,81 @@ class KubernetesDestinationPlugin(DestinationPlugin):
|
|||
author_url = 'https://github.com/mik373/lemur'
|
||||
|
||||
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',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'required': False,
|
||||
'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?',
|
||||
'helpMessage': 'Must be a valid Kubernetes server URL!',
|
||||
'default': 'https://kubernetes.default'
|
||||
},
|
||||
{
|
||||
'name': 'kubernetesAuthToken',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'required': False,
|
||||
'validation': '[0-9a-zA-Z-_.]+',
|
||||
'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',
|
||||
'type': 'textarea',
|
||||
'required': True,
|
||||
'required': False,
|
||||
'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END 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',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'required': False,
|
||||
'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?',
|
||||
'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):
|
||||
|
@ -120,60 +195,90 @@ class KubernetesDestinationPlugin(DestinationPlugin):
|
|||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
|
||||
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)
|
||||
|
||||
k8s_api = K8sSession(k8_bearer, k8_cert)
|
||||
|
||||
secret_format = self.get_option('secretFormat', options)
|
||||
k8s_api = K8sSession(
|
||||
self.k8s_bearer(options),
|
||||
self.k8s_cert(options)
|
||||
)
|
||||
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:
|
||||
current_app.logger.exception("Exception in upload: {}".format(e), exc_info=True)
|
||||
raise
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
def __init__(self, bearer, cert):
|
||||
def __init__(self, bearer, cert_file):
|
||||
super(K8sSession, self).__init__()
|
||||
|
||||
self.headers.update({
|
||||
'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:
|
||||
text_file.write(cert)
|
||||
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream,
|
||||
verify, cert, json)
|
||||
return super(K8sSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout,
|
||||
allow_redirects, proxies, hooks, stream, verify, cert, json)
|
||||
|
|
Loading…
Reference in New Issue