From 060c78fd91241af1a638b570b769db133a9e7f04 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Mon, 10 Dec 2018 15:33:04 -0800 Subject: [PATCH 1/7] Fix Kubernetes Destination Plugin The Kubernetes plugin was broken. There were two major issues: * The server certificate was entered in a string input making it impossible (as far as I know) to enter a valid PEM certificate. * The base64 encoding calls were passing strings where bytes were expected. The fix to the first issue depends on #2218 and a change in the options structure. I've also included some improved input validation and logging. --- lemur/plugins/lemur_kubernetes/plugin.py | 92 +++++++++++++++--------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index ee466596..a640a677 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -11,12 +11,14 @@ .. moduleauthor:: Mikhail Khodorovskiy """ import base64 -import os -import urllib -import requests import itertools +import os -from lemur.certificates.models import Certificate +import requests +from flask import current_app + +from lemur.common.defaults import common_name +from lemur.common.utils import parse_certificate from lemur.plugins.bases import DestinationPlugin DEFAULT_API_VERSION = 'v1' @@ -26,21 +28,32 @@ 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) create_resp = k8s_api.post(url, json=data) + current_app.logger.debug("K8S POST response: %s", create_resp) if 200 <= create_resp.status_code <= 299: return None - elif create_resp.json()['reason'] != 'AlreadyExists': - return create_resp.content + else: + json = create_resp.json() + if 'reason' in json: + if json['reason'] != 'AlreadyExists': + return create_resp.content + else: + return create_resp.content - update_resp = k8s_api.put(_resolve_uri(k8s_base_uri, namespace, kind, name), json=data) + url = _resolve_uri(k8s_base_uri, namespace, kind, name) + current_app.logger.debug("K8S PUT request URL: %s", url) + + update_resp = k8s_api.put(url, json=data) + current_app.logger.debug("K8S PUT response: %s", update_resp) if not 200 <= update_resp.status_code <= 299: return update_resp.content - return + return None def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,): @@ -61,6 +74,12 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V ])) +# Performs Base64 encoding of string to string using the base64.b64encode() function +# which encodes bytes to bytes. +def base64encode(string): + return base64.b64encode(string.encode()).decode() + + class KubernetesDestinationPlugin(DestinationPlugin): title = 'Kubernetes' slug = 'kubernetes-destination' @@ -74,28 +93,28 @@ class KubernetesDestinationPlugin(DestinationPlugin): 'name': 'kubernetesURL', 'type': 'str', 'required': True, - 'validation': '@(https?|http)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s]*)?$@iS', + 'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?', 'helpMessage': 'Must be a valid Kubernetes server URL!', }, { 'name': 'kubernetesAuthToken', 'type': 'str', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '[0-9a-zA-Z-_.]+', 'helpMessage': 'Must be a valid Kubernetes server Token!', }, { 'name': 'kubernetesServerCertificate', - 'type': 'str', + 'type': 'textarea', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END CERTIFICATE-----', 'helpMessage': 'Must be a valid Kubernetes server Certificate!', }, { 'name': 'kubernetesNamespace', 'type': 'str', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?', 'helpMessage': 'Must be a valid Kubernetes Namespace!', }, @@ -106,33 +125,38 @@ class KubernetesDestinationPlugin(DestinationPlugin): def upload(self, name, body, private_key, cert_chain, options, **kwargs): - 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) + 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) + k8s_api = K8sSession(k8_bearer, k8_cert) - cert = Certificate(body=body) + cn = common_name(parse_certificate(body)) - # in the future once runtime properties can be passed-in - use passed-in secret name - secret_name = 'certs-' + urllib.quote_plus(cert.name) + # 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': base64.b64encode(body + private_key), - 'ca.crt': base64.b64encode(cert_chain), - 'service.key': base64.b64encode(private_key), - 'service.crt': base64.b64encode(body), - } - }) + 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") + raise e if err is not None: + current_app.logger.debug("Error deploying resource: %s", err) raise Exception("Error uploading secret: " + err) From bc621c14680b42ca3d98ebc083b973f3210621b9 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Wed, 12 Dec 2018 13:25:36 -0800 Subject: [PATCH 2/7] Improve the Kubernetes Destination plugin The plugin now supports loading details from local files rather than requiring them to be entered through the UI. This is especially relaent when Lemur is deployed on Kubernetes as the certificate, token, and current namespace will be injected into the pod. The location these details are injected are the defaults if no configuration details are supplied. The plugin now supports deploying the secret in three different formats: * Full - matches the formate used by the plugin prior to these changes. * TLS - creates a secret of type kubernetes.io/tls and includes the certificate chain and private key, this format is used by many kubernetes features. * Certificate - creates a secret containing only the certificate chain, suitable for use as trust authority where private keys should _NOT_ be deployed. The deployed secret can now have a name set through the configuration options; the setting allows the insertion of the placeholder '{common_name}' which will be replaced by the certificate's common name value. Debug level logging has been added. --- lemur/plugins/lemur_kubernetes/plugin.py | 187 ++++++++++++++++++----- 1 file changed, 149 insertions(+), 38 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index a640a677..25ce8757 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -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) @@ -56,11 +55,12 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): return None -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): @@ -80,6 +80,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' @@ -89,35 +118,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): @@ -126,31 +201,31 @@ 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) + 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)) - # in the future once runtime properties can be passed-in - use passed-in secret name - secret_name = 'certs-' + cn + 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 + ) - 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") raise e @@ -159,27 +234,63 @@ class KubernetesDestinationPlugin(DestinationPlugin): current_app.logger.debug("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) From e7313da03e5d234b8829c7981d654a6e04dfb6b3 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Tue, 18 Dec 2018 22:24:48 -0500 Subject: [PATCH 3/7] Minor changes for code review suggestions. --- lemur/plugins/lemur_kubernetes/plugin.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index a640a677..4601592a 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -35,14 +35,8 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): if 200 <= create_resp.status_code <= 299: return None - - else: - json = create_resp.json() - if 'reason' in json: - if json['reason'] != 'AlreadyExists': - return create_resp.content - else: - return create_resp.content + elif create_resp.json().get('reason', '') != 'AlreadyExists': + return create_resp.content url = _resolve_uri(k8s_base_uri, namespace, kind, name) current_app.logger.debug("K8S PUT request URL: %s", url) @@ -53,7 +47,7 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): if not 200 <= update_resp.status_code <= 299: return update_resp.content - return None + return def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,): @@ -152,8 +146,8 @@ class KubernetesDestinationPlugin(DestinationPlugin): } }) except Exception as e: - current_app.logger.exception("Exception in upload") - raise 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) From fbf48316b1c39bdbbbb6a6a673be51770462edd2 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Tue, 18 Dec 2018 22:43:32 -0500 Subject: [PATCH 4/7] Minor changes for code review suggestions. --- lemur/plugins/lemur_kubernetes/plugin.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index b111ac2b..30b864eb 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -197,20 +197,14 @@ class KubernetesDestinationPlugin(DestinationPlugin): try: k8_base_uri = self.get_option('kubernetesURL', options) 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, @@ -225,7 +219,7 @@ class KubernetesDestinationPlugin(DestinationPlugin): 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): From 0f2e30cdae07f154c5b5809dc6e5ccee1c5e2158 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Fri, 21 Dec 2018 12:06:52 +0200 Subject: [PATCH 5/7] Deduplicate rows before notification associations unique constraint migration --- lemur/migrations/versions/449c3d5c7299_.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lemur/migrations/versions/449c3d5c7299_.py b/lemur/migrations/versions/449c3d5c7299_.py index 1dcb7ab5..0bc30db1 100644 --- a/lemur/migrations/versions/449c3d5c7299_.py +++ b/lemur/migrations/versions/449c3d5c7299_.py @@ -21,6 +21,14 @@ COLUMNS = ["notification_id", "certificate_id"] def upgrade(): + connection = op.get_bind() + # Delete duplicate entries + connection.execute("""\ + DELETE FROM certificate_notification_associations WHERE ctid NOT IN ( + -- Select the first tuple ID for each (notification_id, certificate_id) combination and keep that + SELECT min(ctid) FROM certificate_notification_associations GROUP BY notification_id, certificate_id + ) + """) op.create_unique_constraint(CONSTRAINT_NAME, TABLE, COLUMNS) From 72f6fdb17d3ad0bba4796fdc668db739246aa36b Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Wed, 19 Dec 2018 17:59:48 +0200 Subject: [PATCH 6/7] Properly handle Unicode in issuer name sanitization If the point of sanitization is to get rid of all non-alphanumeric characters then Unicode characters should probably be forbidden too. We can re-use the same sanitization function as used for cert 'name' --- lemur/common/defaults.py | 38 +++++++++++++++++------------------- lemur/tests/conftest.py | 7 ++++++- lemur/tests/test_defaults.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/lemur/common/defaults.py b/lemur/common/defaults.py index e9bbc6e6..72e863c1 100644 --- a/lemur/common/defaults.py +++ b/lemur/common/defaults.py @@ -7,18 +7,21 @@ from lemur.extensions import sentry from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE -def text_to_slug(value): - """Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters.""" +def text_to_slug(value, joiner='-'): + """ + Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters. + A series of non-alphanumeric characters is replaced with the joiner character. + """ # Strip all character accents: decompose Unicode characters and then drop combining chars. value = ''.join(c for c in unicodedata.normalize('NFKD', value) if not unicodedata.combining(c)) - # Replace all remaining non-alphanumeric characters with '-'. Multiple characters get collapsed into a single dash. - # Except, keep 'xn--' used in IDNA domain names as is. - value = re.sub(r'[^A-Za-z0-9.]+(? Date: Fri, 21 Dec 2018 12:33:47 -0800 Subject: [PATCH 7/7] Update plugin.py --- lemur/plugins/lemur_kubernetes/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index 8de155c3..30b864eb 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -73,6 +73,7 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V def base64encode(string): return base64.b64encode(string.encode()).decode() + def build_secret(secret_format, secret_name, body, private_key, cert_chain): secret = { 'apiVersion': 'v1', @@ -101,6 +102,7 @@ def build_secret(secret_format, secret_name, body, private_key, cert_chain): } return secret + class KubernetesDestinationPlugin(DestinationPlugin): title = 'Kubernetes' slug = 'kubernetes-destination'