From 8b5d5e22ee31fdfb838bf8c48698c7105f4493a3 Mon Sep 17 00:00:00 2001 From: "v.oleynikov" Date: Mon, 15 Apr 2024 15:55:15 +0300 Subject: [PATCH] Add LVM thin provisioning setting Signed-off-by: v.oleynikov --- hooks/common.py | 34 ++ hooks/generate_webhook_certs.py | 42 ++ hooks/lib/__init__.py | 0 hooks/lib/certificate/__init__.py | 0 hooks/lib/certificate/certificate.py | 265 +++++++++++ hooks/lib/certificate/parse.py | 31 ++ hooks/lib/hooks/__init__.py | 0 hooks/lib/hooks/copy_custom_certificate.py | 84 ++++ hooks/lib/hooks/hook.py | 56 +++ hooks/lib/hooks/internal_tls.py | 434 ++++++++++++++++++ hooks/lib/hooks/manage_tenant_secrets.py | 140 ++++++ hooks/lib/module/__init__.py | 0 hooks/lib/module/module.py | 59 +++ hooks/lib/module/values.py | 60 +++ hooks/lib/password_generator/__init__.py | 0 .../password_generator/password_generator.py | 77 ++++ hooks/lib/tests/__init__.py | 0 .../lib/tests/test_copy_custom_certificate.py | 110 +++++ hooks/lib/tests/test_internal_tls_test.py | 159 +++++++ hooks/lib/tests/test_manage_tenant_secrets.py | 102 ++++ hooks/lib/tests/testing.py | 57 +++ hooks/lib/utils.py | 54 +++ images/sds-utils-installer/Dockerfile | 2 +- .../sds-utils-installer/thin-pool-loader.sh | 9 + images/webhooks/src/funcs/init.go | 51 ++ images/webhooks/src/go.mod | 62 +++ images/webhooks/src/go.sum | 175 +++++++ images/webhooks/src/main.go | 45 ++ images/webhooks/src/validators/mcValidator.go | 110 +++++ images/webhooks/werf.inc.yaml | 16 + openapi/config-values.yaml | 4 + openapi/values.yaml | 17 + templates/agent/daemonset.yaml | 13 + templates/webhooks/deployment.yaml | 78 ++++ templates/webhooks/rbac-for-us.yaml | 37 ++ templates/webhooks/secret.yaml | 12 + templates/webhooks/service.yaml | 16 + templates/webhooks/webhook.yaml | 23 + 38 files changed, 2433 insertions(+), 1 deletion(-) create mode 100644 hooks/common.py create mode 100755 hooks/generate_webhook_certs.py create mode 100644 hooks/lib/__init__.py create mode 100644 hooks/lib/certificate/__init__.py create mode 100644 hooks/lib/certificate/certificate.py create mode 100644 hooks/lib/certificate/parse.py create mode 100644 hooks/lib/hooks/__init__.py create mode 100644 hooks/lib/hooks/copy_custom_certificate.py create mode 100644 hooks/lib/hooks/hook.py create mode 100644 hooks/lib/hooks/internal_tls.py create mode 100644 hooks/lib/hooks/manage_tenant_secrets.py create mode 100644 hooks/lib/module/__init__.py create mode 100644 hooks/lib/module/module.py create mode 100644 hooks/lib/module/values.py create mode 100644 hooks/lib/password_generator/__init__.py create mode 100644 hooks/lib/password_generator/password_generator.py create mode 100644 hooks/lib/tests/__init__.py create mode 100644 hooks/lib/tests/test_copy_custom_certificate.py create mode 100644 hooks/lib/tests/test_internal_tls_test.py create mode 100644 hooks/lib/tests/test_manage_tenant_secrets.py create mode 100644 hooks/lib/tests/testing.py create mode 100644 hooks/lib/utils.py create mode 100755 images/sds-utils-installer/thin-pool-loader.sh create mode 100644 images/webhooks/src/funcs/init.go create mode 100644 images/webhooks/src/go.mod create mode 100644 images/webhooks/src/go.sum create mode 100644 images/webhooks/src/main.go create mode 100644 images/webhooks/src/validators/mcValidator.go create mode 100644 images/webhooks/werf.inc.yaml create mode 100644 templates/webhooks/deployment.yaml create mode 100644 templates/webhooks/rbac-for-us.yaml create mode 100644 templates/webhooks/secret.yaml create mode 100644 templates/webhooks/service.yaml create mode 100644 templates/webhooks/webhook.yaml diff --git a/hooks/common.py b/hooks/common.py new file mode 100644 index 00000000..8bdf410f --- /dev/null +++ b/hooks/common.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +from lib.module import module +from typing import Callable +import json +import os +import unittest + + +NAMESPACE = "d8-sds-node-configurator" +MODULE_NAME = "sdsNodeConfigurator" + +def json_load(path: str): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data + +def get_dir_path() -> str: + return os.path.dirname(os.path.abspath(__file__)) diff --git a/hooks/generate_webhook_certs.py b/hooks/generate_webhook_certs.py new file mode 100755 index 00000000..1bd15777 --- /dev/null +++ b/hooks/generate_webhook_certs.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from lib.hooks.internal_tls import GenerateCertificateHook, TlsSecret, default_sans +from lib.module import values as module_values +from deckhouse import hook +from typing import Callable +import common + +def main(): + hook = GenerateCertificateHook( + TlsSecret( + cn="webhooks", + name="webhooks-https-certs", + sansGenerator=default_sans([ + "webhooks", + f"webhooks.{common.NAMESPACE}", + f"webhooks.{common.NAMESPACE}.svc"]), + values_path_prefix=f"{common.MODULE_NAME}.internal.customWebhookCert" + ), + cn="node-configurator", + common_ca=True, + namespace=common.NAMESPACE) + + hook.run() + +if __name__ == "__main__": + main() diff --git a/hooks/lib/__init__.py b/hooks/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/certificate/__init__.py b/hooks/lib/certificate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/certificate/certificate.py b/hooks/lib/certificate/certificate.py new file mode 100644 index 00000000..10a06619 --- /dev/null +++ b/hooks/lib/certificate/certificate.py @@ -0,0 +1,265 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import re +from OpenSSL import crypto +from ipaddress import ip_address +from datetime import datetime, timedelta +from lib.certificate.parse import parse_certificate, get_certificate_san + +class Certificate: + def __init__(self, cn: str, expire: int, key_size: int, algo: str) -> None: + self.key = crypto.PKey() + self.__with_key(algo=algo, size=key_size) + self.cert = crypto.X509() + self.cert.set_version(version=2) + self.cert.get_subject().CN = cn + self.cert.set_serial_number(random.getrandbits(64)) + self.cert.gmtime_adj_notBefore(0) + self.cert.gmtime_adj_notAfter(expire) + + def get_subject(self) -> crypto.X509Name: + return self.cert.get_subject() + + def __with_key(self, algo: str, size: int) -> None: + if algo == "rsa": + self.key.generate_key(crypto.TYPE_RSA, size) + elif algo == "dsa": + self.key.generate_key(crypto.TYPE_DSA, size) + else: + raise Exception(f"Algo {algo} is not support. Only [rsa, dsa]") + + def with_metadata(self, country: str = None, + state: str = None, + locality: str = None, + organisation_name: str = None, + organisational_unit_name: str = None): + """ + Adds subjects to certificate. + + :param country: Optional. The country of the entity. + :type country: :py:class:`str` + + :param state: Optional. The state or province of the entity. + :type state: :py:class:`str` + + :param locality: Optional. The locality of the entity + :type locality: :py:class:`str` + + :param organisation_name: Optional. The organization name of the entity. + :type organisation_name: :py:class:`str` + + :param organisational_unit_name: Optional. The organizational unit of the entity. + :type organisational_unit_name: :py:class:`str` + """ + + if country is not None: + self.cert.get_subject().C = country + if state is not None: + self.cert.get_subject().ST = state + if locality is not None: + self.cert.get_subject().L = locality + if organisation_name is not None: + self.cert.get_subject().O = organisation_name + if organisational_unit_name is not None: + self.cert.get_subject().OU = organisational_unit_name + return self + + def add_extension(self, type_name: str, + critical: bool, + value: str, + subject: crypto.X509 = None, + issuer: crypto.X509 = None): + """ + Adds extensions to certificate. + :param type_name: The name of the type of extension_ to create. + :type type_name: :py:class:`str` + + :param critical: A flag indicating whether this is a critical + extension. + :type critical: :py:class:`bool` + + :param value: The OpenSSL textual representation of the extension's + value. + :type value: :py:class:`str` + + :param subject: Optional X509 certificate to use as subject. + :type subject: :py:class:`crypto.X509` + + :param issuer: Optional X509 certificate to use as issuer. + :type issuer: :py:class:`crypto.X509` + """ + ext = crypto.X509Extension(type_name=str.encode(type_name), + critical=critical, + value=str.encode(value), + subject=subject, + issuer=issuer) + self.cert.add_extensions(extensions=[ext]) + return self + + def generate(self) -> (bytes, bytes): + """ + Generate certificate. + :return: (certificate, key) + :rtype: (:py:data:`bytes`, :py:data:`bytes`) + """ + pub = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + priv = crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key) + return pub, priv + + +class CACertificateGenerator(Certificate): + """ + A class representing a generator CA certificate. + """ + def __sign(self) -> None: + self.cert.set_issuer(self.get_subject()) + self.cert.set_pubkey(self.key) + self.cert.sign(self.key, 'sha256') + + def generate(self) -> (bytes, bytes): + """ + Generate CA certificate. + :return: (ca crt, ca key) + :rtype: (:py:data:`bytes`, :py:data:`bytes`) + """ + self.add_extension(type_name="subjectKeyIdentifier", + critical=False, value="hash", subject=self.cert) + self.add_extension(type_name="authorityKeyIdentifier", + critical=False, value="keyid:always", issuer=self.cert) + self.add_extension(type_name="basicConstraints", + critical=False, value="CA:TRUE") + self.add_extension(type_name="keyUsage", critical=False, + value="keyCertSign, cRLSign, keyEncipherment") + self.__sign() + return super().generate() + + +class CertificateGenerator(Certificate): + """ + A class representing a generator certificate. + """ + def with_hosts(self, *hosts: str): + """ + This function is used to add subject alternative names to a certificate. + It takes a variable number of hosts as parameters, and based on the type of host (IP or DNS). + + :param hosts: Variable number of hosts to be added as subject alternative names to the certificate. + :type hosts: :py:class:`tuple` + """ + alt_names = [] + for h in hosts: + try: + ip_address(h) + alt_names.append(f"IP:{h}") + except ValueError: + if not is_valid_hostname(h): + continue + alt_names.append(f"DNS:{h}") + self.add_extension("subjectAltName", False, ", ".join(alt_names)) + return self + + def __sign(self, ca_subj: crypto.X509Name, ca_key: crypto.PKey) -> None: + self.cert.set_issuer(ca_subj) + self.cert.set_pubkey(self.key) + self.cert.sign(ca_key, 'sha256') + + def generate(self, ca_subj: crypto.X509Name, ca_key: crypto.PKey) -> (bytes, bytes): + """ + Generate certificate. + :param ca_subj: CA subject. + :type ca_subj: :py:class:`crypto.X509Name` + :param ca_key: CA Key. + :type ca_key: :py:class:`crypto.PKey` + :return: (certificate, key) + :rtype: (:py:data:`bytes`, :py:data:`bytes`) + """ + self.__sign(ca_subj, ca_key) + return super().generate() + +def is_valid_hostname(hostname: str) -> bool: + if len(hostname) > 255: + return False + hostname.rstrip(".") + allowed = re.compile("(?!-)[A-Z\d-]{1,63}(? bool: + """ + Check certificate + :param crt: Certificate + :type crt: :py:class:`crypto.X509` + :param cert_outdated_duration: certificate outdated duration + :type cert_outdated_duration: :py:class:`timedelta` + :return: + if timeNow > expire - cert_outdated_duration: + return True + return False + :rtype: :py:class:`bool` + """ + not_after = datetime.strptime( + crt.get_notAfter().decode('ascii'), '%Y%m%d%H%M%SZ') + if datetime.now() > not_after - cert_outdated_duration: + return True + return False + + +def is_outdated_ca(ca: str, cert_outdated_duration: timedelta) -> bool: + """ + Issue a new certificate if there is no CA in the secret. Without CA it is not possible to validate the certificate. + Check CA duration. + :param ca: Raw CA + :type ca: :py:class:`str` + :param cert_outdated_duration: certificate outdated duration + :type cert_outdated_duration: :py:class:`timedelta` + :rtype: :py:class:`bool` + """ + if len(ca) == 0: + return True + crt = parse_certificate(ca) + return cert_renew_deadline_exceeded(crt, cert_outdated_duration) + + +def is_irrelevant_cert(crt_data: str, sans: list, cert_outdated_duration: timedelta) -> bool: + """ + Check certificate duration and SANs list + :param crt_data: Raw certificate + :type crt_data: :py:class:`str` + :param sans: List of sans. + :type sans: :py:class:`list` + :param cert_outdated_duration: certificate outdated duration + :type cert_outdated_duration: :py:class:`timedelta` + :rtype: :py:class:`bool` + """ + if len(crt_data) == 0: + return True + crt = parse_certificate(crt_data) + if cert_renew_deadline_exceeded(crt, cert_outdated_duration): + return True + alt_names = [] + for san in sans: + try: + ip_address(san) + alt_names.append(f"IP Address:{san}") + except ValueError: + alt_names.append(f"DNS:{san}") + cert_sans = get_certificate_san(crt) + cert_sans.sort() + alt_names.sort() + if cert_sans != alt_names: + return True + return False \ No newline at end of file diff --git a/hooks/lib/certificate/parse.py b/hooks/lib/certificate/parse.py new file mode 100644 index 00000000..fd88c0d8 --- /dev/null +++ b/hooks/lib/certificate/parse.py @@ -0,0 +1,31 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from OpenSSL import crypto +from pprint import pprint + + +def parse_certificate(crt: str) -> crypto.X509: + return crypto.load_certificate(crypto.FILETYPE_PEM, crt) + + +def get_certificate_san(crt: crypto.X509) -> list[str]: + san = '' + ext_count = crt.get_extension_count() + for i in range(0, ext_count): + ext = crt.get_extension(i) + if 'subjectAltName' in str(ext.get_short_name()): + san = ext.__str__() + return san.split(', ') diff --git a/hooks/lib/hooks/__init__.py b/hooks/lib/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/hooks/copy_custom_certificate.py b/hooks/lib/hooks/copy_custom_certificate.py new file mode 100644 index 00000000..b3f3e835 --- /dev/null +++ b/hooks/lib/hooks/copy_custom_certificate.py @@ -0,0 +1,84 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +from typing import Callable +from lib.module import module +from lib.hooks.hook import Hook + +class CopyCustomCertificatesHook(Hook): + CUSTOM_CERTIFICATES_SNAPSHOT_NAME = "custom_certificates" + def __init__(self, + module_name: str = None): + super().__init__(module_name=module_name) + self.queue = f"/modules/{self.module_name}/copy-custom-certificates" + + def generate_config(self) -> dict: + return { + "configVersion": "v1", + "beforeHelm": 10, + "kubernetes": [ + { + "name": self.CUSTOM_CERTIFICATES_SNAPSHOT_NAME, + "apiVersion": "v1", + "kind": "Secret", + "labelSelector": { + "matchExpressions": [ + { + "key": "owner", + "operator": "NotIn", + "values": ["helm"] + } + ] + }, + "namespace": { + "nameSelector": { + "matchNames": ["d8-system"] + } + }, + "includeSnapshotsFrom": [self.CUSTOM_CERTIFICATES_SNAPSHOT_NAME], + "jqFilter": '{"name": .metadata.name, "data": .data}', + "queue": self.queue, + "keepFullObjectsInMemory": False + }, + ] + } + + def reconcile(self) -> Callable[[hook.Context], None]: + def r(ctx: hook.Context) -> None: + custom_certificates = {} + for s in ctx.snapshots.get(self.CUSTOM_CERTIFICATES_SNAPSHOT_NAME, []): + custom_certificates[s["filterResult"]["name"]] = s["filterResult"]["data"] + if len(custom_certificates) == 0: + return + + https_mode = module.get_https_mode(module_name=self.module_name, + values=ctx.values) + path = f"{self.module_name}.internal.customCertificateData" + if https_mode != "CustomCertificate": + self.delete_value(path, ctx.values) + return + + raw_secret_name = module.get_values_first_defined(ctx.values, + f"{self.module_name}.https.customCertificate.secretName", + "global.modules.https.customCertificate.secretName") + secret_name = str(raw_secret_name or "") + secret_data = custom_certificates.get(secret_name) + if secret_data is None: + print( + f"Custom certificate secret name is configured, but secret d8-system/{secret_name} doesn't exist") + return + self.set_value(path, ctx.values, secret_data) + return r \ No newline at end of file diff --git a/hooks/lib/hooks/hook.py b/hooks/lib/hooks/hook.py new file mode 100644 index 00000000..e412faec --- /dev/null +++ b/hooks/lib/hooks/hook.py @@ -0,0 +1,56 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +from typing import Callable +from lib.module import module +from lib.module import values as module_values +import yaml + +class Hook: + def __init__(self, module_name: str = None) -> None: + self.module_name = self.get_module_name(module_name) + + def generate_config(self): + pass + + @staticmethod + def get_value(path: str, values: dict, default=None): + return module_values.get_value(path, values, default) + + @staticmethod + def set_value(path: str, values: dict, value: str) -> None: + return module_values.set_value(path, values, value) + + @staticmethod + def delete_value(path: str, values: dict) -> None: + return module_values.delete_value(path, values) + + @staticmethod + def get_module_name(module_name: str) -> str: + if module_name is not None: + return module_name + return module.get_module_name() + + def reconcile(self) -> Callable[[hook.Context], None]: + def r(ctx: hook.Context) -> None: + pass + return r + + def run(self) -> None: + conf = self.generate_config() + if isinstance(conf, dict): + conf = yaml.dump(conf) + hook.run(func=self.reconcile(), config=conf) diff --git a/hooks/lib/hooks/internal_tls.py b/hooks/lib/hooks/internal_tls.py new file mode 100644 index 00000000..3b351c65 --- /dev/null +++ b/hooks/lib/hooks/internal_tls.py @@ -0,0 +1,434 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +from datetime import timedelta +from OpenSSL import crypto +from typing import Callable +from lib.hooks.hook import Hook +import lib.utils as utils +import lib.certificate.certificate as certificate + +PUBLIC_DOMAIN_PREFIX = "%PUBLIC_DOMAIN%://" +CLUSTER_DOMAIN_PREFIX = "%CLUSTER_DOMAIN%://" + + +KEY_USAGES = { + 0: "digitalSignature", + 1: "nonRepudiation", + 2: "keyEncipherment", + 3: "dataEncipherment", + 4: "keyAgreement", + 5: "keyCertSign", + 6: "cRLSign", + 7: "encipherOnly", + 8: "decipherOnly" +} + +EXTENDED_KEY_USAGES = { + 0: "serverAuth", + 1: "clientAuth", + 2: "codeSigning", + 3: "emailProtection", + 4: "OCSPSigning" +} + +class TlsSecret: + def __init__(self, + cn: str, + name: str, + sansGenerator: Callable[[list[str]], Callable[[hook.Context], list[str]]], + values_path_prefix: str, + key_usages: list[str] = [KEY_USAGES[2], KEY_USAGES[5]], + extended_key_usages: list[str] = [EXTENDED_KEY_USAGES[0]]): + self.cn = cn + self.name = name + self.sansGenerator = sansGenerator + self.values_path_prefix = values_path_prefix + self.key_usages = key_usages + self.extended_key_usages = extended_key_usages + +class GenerateCertificateHook(Hook): + """ + Config for the hook that generates certificates. + """ + SNAPSHOT_SECRETS_NAME = "secrets" + SNAPSHOT_SECRETS_CHECK_NAME = "secretsCheck" + + def __init__(self, *tls_secrets: TlsSecret, + cn: str, + namespace: str, + module_name: str = None, + common_ca: bool = False, + before_hook_check: Callable[[hook.Context], bool] = None, + expire: int = 31536000, + key_size: int = 4096, + algo: str = "rsa", + cert_outdated_duration: timedelta = timedelta(days=30), + country: str = None, + state: str = None, + locality: str = None, + organisation_name: str = None, + organisational_unit_name: str = None) -> None: + super().__init__(module_name=module_name) + self.cn = cn + self.tls_secrets = tls_secrets + self.namespace = namespace + self.common_ca = common_ca + self.before_hook_check = before_hook_check + self.expire = expire + self.key_size = key_size + self.algo = algo + self.cert_outdated_duration = cert_outdated_duration + self.country = country + self.state = state + self.locality = locality + self.organisation_name = organisation_name + self.organisational_unit_name = organisational_unit_name + self.secret_names = [secret.name for secret in self.tls_secrets] + self.queue = f"/modules/{self.module_name}/generate-certs" + """ + :param module_name: Module name + :type module_name: :py:class:`str` + + :param cn: Certificate common Name. often it is module name + :type cn: :py:class:`str` + + :param sansGenerator: Function which returns list of domain to include into cert. Use default_sans + :type sansGenerator: :py:class:`function` + + :param namespace: Namespace for TLS secret. + :type namespace: :py:class:`str` + + :param tls_secret_name: TLS secret name. + Secret must be TLS secret type https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets. + CA certificate MUST set to ca.crt key. + :type tls_secret_name: :py:class:`str` + + :param values_path_prefix: Prefix full path to store CA certificate TLS private key and cert. + full paths will be + values_path_prefix + .`ca` - CA certificate + values_path_prefix + .`crt` - TLS private key + values_path_prefix + .`key` - TLS certificate + Example: values_path_prefix = 'virtualization.internal.dvcrCert' + Data in values store as plain text + :type values_path_prefix: :py:class:`str` + + :param key_usages: Optional. key_usages specifies valid usage contexts for keys. + :type key_usages: :py:class:`list` + + :param extended_key_usages: Optional. extended_key_usages specifies valid usage contexts for keys. + :type extended_key_usages: :py:class:`list` + + :param before_hook_check: Optional. Runs check function before hook execution. Function should return boolean 'continue' value + if return value is false - hook will stop its execution + if return value is true - hook will continue + :type before_hook_check: :py:class:`function` + + :param expire: Optional. Validity period of SSL certificates. + :type expire: :py:class:`int` + + :param key_size: Optional. Key Size. + :type key_size: :py:class:`int` + + :param algo: Optional. Key generation algorithm. Supports only rsa and dsa. + :type algo: :py:class:`str` + + :param cert_outdated_duration: Optional. (expire - cert_outdated_duration) is time to regenerate the certificate. + :type cert_outdated_duration: :py:class:`timedelta` + """ + + def generate_config(self) -> dict: + return { + "configVersion": "v1", + "beforeHelm": 5, + "kubernetes": [ + { + "name": self.SNAPSHOT_SECRETS_NAME, + "apiVersion": "v1", + "kind": "Secret", + "nameSelector": { + "matchNames": self.secret_names + }, + "namespace": { + "nameSelector": { + "matchNames": [self.namespace] + } + }, + "includeSnapshotsFrom": [self.SNAPSHOT_SECRETS_NAME], + "jqFilter": '{"name": .metadata.name, "data": .data}', + "queue": self.queue, + "keepFullObjectsInMemory": False + }, + ], + "schedule": [ + { + "name": self.SNAPSHOT_SECRETS_CHECK_NAME, + "crontab": "42 4 * * *" + } + ] + } + + def reconcile(self) -> Callable[[hook.Context], None]: + def r(ctx: hook.Context) -> None: + if self.before_hook_check is not None: + passed = self.before_hook_check(ctx) + if not passed: + return + + regenerate_all = False + secrets_from_snaps = {} + diff_secrets = [] + if len(ctx.snapshots.get(self.SNAPSHOT_SECRETS_NAME, [])) == 0: + regenerate_all = True + else: + for snap in ctx.snapshots[self.SNAPSHOT_SECRETS_NAME]: + secrets_from_snaps[snap["filterResult"]["name"]] = snap["filterResult"]["data"] + for secret in self.tls_secrets: + if secrets_from_snaps.get(secret.name) is None: + diff_secrets.append(secret.name) + + if self.common_ca and not regenerate_all: + if len(diff_secrets) > 0: + regenerate_all = True + else: + for secret in self.tls_secrets: + data = secrets_from_snaps[secret.name] + if self.is_outdated_ca(utils.base64_decode(data.get("ca.crt", ""))): + regenerate_all = True + break + sans = secret.sansGenerator(ctx) + if self.is_irrelevant_cert(utils.base64_decode(data.get("tls.crt", "")), sans): + regenerate_all = True + break + + if regenerate_all: + if self.common_ca: + ca = self.__get_ca_generator() + ca_crt, _ = ca.generate() + for secret in self.tls_secrets: + sans = secret.sansGenerator(ctx) + print(f"Generate new certififcates for secret {secret.name}.") + tls_data = self.generate_selfsigned_tls_data_with_ca(cn=secret.cn, + ca=ca, + ca_crt=ca_crt, + sans=sans, + key_usages=secret.key_usages, + extended_key_usages=secret.extended_key_usages) + self.set_value(secret.values_path_prefix, ctx.values, tls_data) + return + + for secret in self.tls_secrets: + sans = secret.sansGenerator(ctx) + print(f"Generate new certififcates for secret {secret.name}.") + tls_data = self.generate_selfsigned_tls_data(cn=secret.cn, + sans=sans, + key_usages=secret.key_usages, + extended_key_usages=secret.extended_key_usages) + self.set_value(secret.values_path_prefix, ctx.values, tls_data) + return + + for secret in self.tls_secrets: + data = secrets_from_snaps[secret.name] + sans = secret.sansGenerator(ctx) + cert_outdated = self.is_irrelevant_cert( + utils.base64_decode(data.get("tls.crt", "")), sans) + + tls_data = {} + if cert_outdated or data.get("tls.key", "") == "": + print(f"Certificates from secret {secret.name} is invalid. Generate new certififcates.") + tls_data = self.generate_selfsigned_tls_data(cn=secret.cn, + sans=sans, + key_usages=secret.key_usages, + extended_key_usages=secret.extended_key_usages) + else: + tls_data = { + "ca": data["ca.crt"], + "crt": data["tls.crt"], + "key": data["tls.key"] + } + self.set_value(secret.values_path_prefix, ctx.values, tls_data) + return r + + def __get_ca_generator(self) -> certificate.CACertificateGenerator: + return certificate.CACertificateGenerator(cn=f"{self.cn}", + expire=self.expire, + key_size=self.key_size, + algo=self.algo) + + def generate_selfsigned_tls_data_with_ca(self, + cn: str, + ca: certificate.CACertificateGenerator, + ca_crt: bytes, + sans: list[str], + key_usages: list[str], + extended_key_usages: list[str]) -> dict[str, str]: + """ + Generate self signed certificate. + :param cn: certificate common name. + :param ca: Ca certificate generator. + :type ca: :py:class:`certificate.CACertificateGenerator` + :param ca_crt: bytes. + :type ca_crt: :py:class:`bytes` + :param sans: List of sans. + :type sans: :py:class:`list` + :param key_usages: List of key_usages. + :type key_usages: :py:class:`list` + :param extended_key_usages: List of extended_key_usages. + :type extended_key_usages: :py:class:`list` + Example: { + "ca": "encoded in base64", + "crt": "encoded in base64", + "key": "encoded in base64" + } + :rtype: :py:class:`dict[str, str]` + """ + cert = certificate.CertificateGenerator(cn=cn, + expire=self.expire, + key_size=self.key_size, + algo=self.algo) + if len(key_usages) > 0: + key_usages = ", ".join(key_usages) + cert.add_extension(type_name="keyUsage", + critical=False, value=key_usages) + if len(extended_key_usages) > 0: + extended_key_usages = ", ".join(extended_key_usages) + cert.add_extension(type_name="extendedKeyUsage", + critical=False, value=extended_key_usages) + crt, key = cert.with_metadata(country=self.country, + state=self.state, + locality=self.locality, + organisation_name=self.organisation_name, + organisational_unit_name=self.organisational_unit_name + ).with_hosts(*sans).generate(ca_subj=ca.get_subject(), + ca_key=ca.key) + return {"ca": utils.base64_encode(ca_crt), + "crt": utils.base64_encode(crt), + "key": utils.base64_encode(key)} + + def generate_selfsigned_tls_data(self, + cn: str, + sans: list[str], + key_usages: list[str], + extended_key_usages: list[str]) -> dict[str, str]: + """ + Generate self signed certificate. + :param cn: certificate common name. + :param sans: List of sans. + :type sans: :py:class:`list` + :param key_usages: List of key_usages. + :type key_usages: :py:class:`list` + :param extended_key_usages: List of extended_key_usages. + :type extended_key_usages: :py:class:`list` + Example: { + "ca": "encoded in base64", + "crt": "encoded in base64", + "key": "encoded in base64" + } + :rtype: :py:class:`dict[str, str]` + """ + ca = self.__get_ca_generator() + ca_crt, _ = ca.generate() + return self.generate_selfsigned_tls_data_with_ca(cn=cn, + ca=ca, + ca_crt=ca_crt, + sans=sans, + key_usages=key_usages, + extended_key_usages=extended_key_usages) + + def is_irrelevant_cert(self, crt_data: str, sans: list) -> bool: + """ + Check certificate duration and SANs list + :param crt_data: Raw certificate + :type crt_data: :py:class:`str` + :param sans: List of sans. + :type sans: :py:class:`list` + :rtype: :py:class:`bool` + """ + return certificate.is_irrelevant_cert(crt_data, sans, self.cert_outdated_duration) + + def is_outdated_ca(self, ca: str) -> bool: + """ + Issue a new certificate if there is no CA in the secret. Without CA it is not possible to validate the certificate. + Check CA duration. + :param ca: Raw CA + :type ca: :py:class:`str` + :rtype: :py:class:`bool` + """ + return certificate.is_outdated_ca(ca, self.cert_outdated_duration) + + def cert_renew_deadline_exceeded(self, crt: crypto.X509) -> bool: + """ + Check certificate + :param crt: Certificate + :type crt: :py:class:`crypto.X509` + :return: + if timeNow > expire - cert_outdated_duration: + return True + return False + :rtype: :py:class:`bool` + """ + return certificate.cert_renew_deadline_exceeded(crt, self.cert_outdated_duration) + +def default_sans(sans: list[str]) -> Callable[[hook.Context], list[str]]: + """ + Generate list of sans for certificate + :param sans: List of alt names. + :type sans: :py:class:`list[str]` + cluster_domain_san(san) to generate sans with respect of cluster domain (e.g.: "app.default.svc" with "cluster.local" value will give: app.default.svc.cluster.local + + public_domain_san(san) + """ + def generate_sans(ctx: hook.Context) -> list[str]: + res = ["localhost", "127.0.0.1"] + public_domain = str(ctx.values["global"]["modules"].get( + "publicDomainTemplate", "")) + cluster_domain = str( + ctx.values["global"]["discovery"].get("clusterDomain", "")) + for san in sans: + san.startswith(PUBLIC_DOMAIN_PREFIX) + if san.startswith(PUBLIC_DOMAIN_PREFIX) and public_domain != "": + san = get_public_domain_san(san, public_domain) + elif san.startswith(CLUSTER_DOMAIN_PREFIX) and cluster_domain != "": + san = get_cluster_domain_san(san, cluster_domain) + res.append(san) + return res + return generate_sans + + +def cluster_domain_san(san: str) -> str: + """ + Create template to enrich specified san with a cluster domain + :param san: San. + :type sans: :py:class:`str` + """ + return CLUSTER_DOMAIN_PREFIX + san.rstrip('.') + + +def public_domain_san(san: str) -> str: + """ + Create template to enrich specified san with a public domain + :param san: San. + :type sans: :py:class:`str` + """ + return PUBLIC_DOMAIN_PREFIX + san.rstrip('.') + + +def get_public_domain_san(san: str, public_domain: str) -> str: + return f"{san.lstrip(PUBLIC_DOMAIN_PREFIX)}.{public_domain}" + + +def get_cluster_domain_san(san: str, cluster_domain: str) -> str: + return f"{san.lstrip(CLUSTER_DOMAIN_PREFIX)}.{cluster_domain}" diff --git a/hooks/lib/hooks/manage_tenant_secrets.py b/hooks/lib/hooks/manage_tenant_secrets.py new file mode 100644 index 00000000..36d817b4 --- /dev/null +++ b/hooks/lib/hooks/manage_tenant_secrets.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +from typing import Callable +from lib.hooks.hook import Hook + +class ManageTenantSecretsHook(Hook): + POD_SNAPSHOT_NAME = "pods" + SECRETS_SNAPSHOT_NAME = "secrets" + NAMESPACE_SNAPSHOT_NAME = "namespaces" + + def __init__(self, + source_namespace: str, + source_secret_name: str, + pod_labels_to_follow: dict, + destination_secret_labels: dict = {}, + module_name: str = None): + super().__init__(module_name=module_name) + self.source_namespace = source_namespace + self.source_secret_name = source_secret_name + self.pod_labels_to_follow = pod_labels_to_follow + self.destination_secret_labels = destination_secret_labels + self.module_name = module_name + self.queue = f"/modules/{module_name}/manage-tenant-secrets" + + def generate_config(self) -> dict: + return { + "configVersion": "v1", + "kubernetes": [ + { + "name": self.POD_SNAPSHOT_NAME, + "apiVersion": "v1", + "kind": "Pod", + "includeSnapshotsFrom": [ + self.POD_SNAPSHOT_NAME, + self.SECRETS_SNAPSHOT_NAME, + self.NAMESPACE_SNAPSHOT_NAME + ], + "labelSelector": { + "matchLabels": self.pod_labels_to_follow + }, + "jqFilter": '{"namespace": .metadata.namespace}', + "queue": self.queue, + "keepFullObjectsInMemory": False + }, + { + "name": self.SECRETS_SNAPSHOT_NAME, + "apiVersion": "v1", + "kind": "Secret", + "includeSnapshotsFrom": [ + self.POD_SNAPSHOT_NAME, + self.SECRETS_SNAPSHOT_NAME, + self.NAMESPACE_SNAPSHOT_NAME + ], + "nameSelector": { + "matchNames": [self.source_secret_name] + }, + "jqFilter": '{"data": .data, "namespace": .metadata.namespace, "type": .type}', + "queue": self.queue, + "keepFullObjectsInMemory": False + }, + { + "name": self.NAMESPACE_SNAPSHOT_NAME, + "apiVersion": "v1", + "kind": "Secret", + "includeSnapshotsFrom": [ + self.POD_SNAPSHOT_NAME, + self.SECRETS_SNAPSHOT_NAME, + self.NAMESPACE_SNAPSHOT_NAME + ], + "jqFilter": '{"name": .metadata.name, "isTerminating": any(.metadata; .deletionTimestamp != null)}', + "queue": self.queue, + "keepFullObjectsInMemory": False + } + ] + } + + def generate_secret(self, namespace: str, data: dict, secret_type: str) -> dict: + return { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": self.source_secret_name, + "namespace": namespace, + "labels": self.destination_secret_labels + }, + "data": data, + "type": secret_type + } + + def reconcile(self) -> Callable[[hook.Context], None]: + def r(ctx: hook.Context) -> None: + pod_namespaces = set([p["filterResult"]["namespace"] for p in ctx.snapshots.get(self.POD_SNAPSHOT_NAME, [])]) + secrets = ctx.snapshots.get(self.SECRETS_SNAPSHOT_NAME, []) + for ns in ctx.snapshots.get(self.NAMESPACE_SNAPSHOT_NAME, []): + if ns["filterResult"]["isTerminating"]: + pod_namespaces.discard(ns["filterResult"]["name"]) + data, secret_type, secrets_by_ns = "", "", {} + for s in secrets: + if s["filterResult"]["namespace"] == self.source_namespace: + data = s["filterResult"]["data"] + secret_type = s["filterResult"]["type"] + continue + secrets_by_ns[s["filterResult"]["namespace"]] = s["filterResult"]["data"] + + if len(data) == 0 or len(secret_type) == 0: + print(f"Registry secret {self.source_namespace}/{self.source_secret_name} not found. Skip") + return + + for ns in pod_namespaces: + secret_data = secrets_by_ns.get(ns, "") + if (secret_data != data) and (ns != self.source_namespace): + secret = self.generate_secret(namespace=ns, + data=data, + secret_type=secret_type) + print(f"Create secret {ns}/{self.source_secret_name}.") + ctx.kubernetes.create_or_update(secret) + for ns in secrets_by_ns: + if (ns in pod_namespaces) or (ns == self.source_namespace): + continue + print(f"Delete secret {ns}/{self.source_secret_name}.") + ctx.kubernetes.delete(kind="Secret", + namespace=ns, + name=self.source_secret_name) + return r + diff --git a/hooks/lib/module/__init__.py b/hooks/lib/module/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/module/module.py b/hooks/lib/module/module.py new file mode 100644 index 00000000..25e034de --- /dev/null +++ b/hooks/lib/module/module.py @@ -0,0 +1,59 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lib.module import values as module_values +import re +import os + +def get_values_first_defined(values: dict, *keys): + return _get_first_defined(values, keys) + +def _get_first_defined(values: dict, keys: tuple): + for i in range(len(keys)): + if (val := module_values.get_value(path=keys[i], values=values)) is not None: + return val + return + +def get_https_mode(module_name: str, values: dict) -> str: + module_path = f"{module_name}.https.mode" + global_path = "global.modules.https.mode" + https_mode = get_values_first_defined(values, module_path, global_path) + if https_mode is not None: + return str(https_mode) + raise Exception("https mode is not defined") + +def get_module_name() -> str: + module = "" + file_path = os.path.abspath(__file__) + external_modules_dir = os.getenv("EXTERNAL_MODULES_DIR") + for dir in os.getenv("MODULES_DIR").split(":"): + if dir.startswith(external_modules_dir): + dir = external_modules_dir + if file_path.startswith(dir): + module = re.sub(f"{dir}/?\d?\d?\d?-?", "", file_path, 1).split("/")[0] + # /deckhouse/external-modules/virtualization/mr/hooks/hook_name.py + # {-------------------------- file_path --------------------------} + # {------ MODULES_DIR ------}{---------- regexp result ----------}} + # virtualization/mr/hooks/hook_name.py + # {-module-name-}{---------------------} + # or + # /deckhouse/modules/900-virtualization/hooks/hook_name.py + # {---------------------- file_path ----------------------} + # {-- MODULES_DIR --}{---{-------- regexp result --------}} + # virtualization/hooks/hook_name.py + # {-module-name-}{-----------------} + + break + return module \ No newline at end of file diff --git a/hooks/lib/module/values.py b/hooks/lib/module/values.py new file mode 100644 index 00000000..449e4441 --- /dev/null +++ b/hooks/lib/module/values.py @@ -0,0 +1,60 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def get_value(path: str, values: dict, default=None): + def get(keys: list, values: dict, default): + if len(keys) == 1: + if not isinstance(values, dict): + return default + return values.get(keys[0], default) + if not isinstance(values, dict) or values.get(keys[0]) is None: + return default + if values.get(keys[0]) is None: + return default + return get(keys[1:], values[keys[0]], default) + keys = path.lstrip(".").split(".") + return get(keys, values, default) + +def set_value(path: str, values: dict, value) -> None: + """ + Functions for save value to dict. + + Example: + path = "virtualization.internal.dvcr.cert" + values = {"virtualization": {"internal": {}}} + value = "{"ca": "ca", "crt"="tlscrt", "key"="tlskey"}" + + result values = {"virtualization": {"internal": {"dvcr": {"cert": {"ca": "ca", "crt":"tlscrt", "key":"tlskey"}}}}} + """ + def set(keys: list, values: dict, value): + if len(keys) == 1: + values[keys[0]] = value + return + if values.get(keys[0]) is None: + values[keys[0]] = {} + set(keys[1:], values[keys[0]], value) + keys = path.lstrip(".").split(".") + return set(keys, values, value) + +def delete_value(path: str, values: dict) -> None: + if get_value(path, values) is None: + return + keys = path.lstrip(".").split(".") + def delete(keys: list, values: dict) -> None: + if len(keys) == 1: + values.pop(keys[0]) + return + delete(keys[1:], values[keys[0]]) + return delete(keys, values) \ No newline at end of file diff --git a/hooks/lib/password_generator/__init__.py b/hooks/lib/password_generator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/password_generator/password_generator.py b/hooks/lib/password_generator/password_generator.py new file mode 100644 index 00000000..df63ea82 --- /dev/null +++ b/hooks/lib/password_generator/password_generator.py @@ -0,0 +1,77 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import string + +def generate_random_string(length: int, letters: str) -> str: + return ''.join(random.choice(letters) for i in range(length)) + +SYMBOLS = "[]{}<>()=-_!@#$%^&*.," + +def num(length: int) -> str: + """ + Generates a random string of the given length out of numeric characters. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, string.digits) + +def alpha(length: int) -> str: + """ + Generates a random string of the given length out of alphabetic characters. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, string.ascii_letters) + +def symbols(length: int) -> str: + """ + Generates a random string of the given length out of symbols. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, SYMBOLS) + + +def alpha_num(length: int) -> str: + """ + Generates a random string of the given length out of alphanumeric characters. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, string.ascii_letters + string.digits) + +def alpha_num_lower_case(length: int) -> str: + """ + Generates a random string of the given length out of alphanumeric characters without UpperCase letters. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, string.ascii_lowercase + string.digits) + +def alpha_num_symbols(length: int) -> str: + """ + Generates a random string of the given length out of alphanumeric characters and symbols. + :param length: length of generate string. + :type length: :py:class:`int` + :rtype: :py:class:`str` + """ + return generate_random_string(length, string.ascii_letters + string.digits + SYMBOLS) diff --git a/hooks/lib/tests/__init__.py b/hooks/lib/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hooks/lib/tests/test_copy_custom_certificate.py b/hooks/lib/tests/test_copy_custom_certificate.py new file mode 100644 index 00000000..c10e9c95 --- /dev/null +++ b/hooks/lib/tests/test_copy_custom_certificate.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lib.tests import testing +from lib.hooks.copy_custom_certificate import CopyCustomCertificatesHook + + +MODULE_NAME = "test" +SECRET_NAME = "secretName" +SECRET_DATA = { + "ca.crt": "CACRT", + "tls.crt": "TLSCRT", + "tls.key": "TLSKEY" + } + +hook = CopyCustomCertificatesHook(module_name=MODULE_NAME) + +binding_context = [ + { + "binding": "binding", + "snapshots": { + hook.CUSTOM_CERTIFICATES_SNAPSHOT_NAME: [ + { + "filterResult": { + "name": SECRET_NAME, + "data": SECRET_DATA + } + }, + { + "filterResult": { + "name": "test", + "data": {} + } + } + ] + } + } +] + +values_add = { + "global": { + "modules": { + "https": { + "mode": "CustomCertificate", + "customCertificate": { + "secretName": "test" + } + } + } + }, + MODULE_NAME: { + "https": { + "customCertificate": { + "secretName": SECRET_NAME + } + }, + "internal": {} + } +} + + +values_delete = { + "global": { + "modules": { + "https": { + "mode": "CertManager" + } + } + }, + MODULE_NAME: { + "internal": { + "customCertificateData": SECRET_DATA + } + } +} + + +class TestCopyCustomCertificateAdd(testing.TestHook): + def setUp(self): + self.func = hook.reconcile() + self.bindind_context = binding_context + self.values = values_add + def test_copy_custom_certificate_adding(self): + self.hook_run() + self.assertGreater(len(self.values[MODULE_NAME]["internal"].get("customCertificateData", {})), 0) + self.assertEqual(self.values[MODULE_NAME]["internal"]["customCertificateData"], SECRET_DATA) + +class TestCopyCustomCertificateDelete(testing.TestHook): + def setUp(self): + self.func = hook.reconcile() + self.bindind_context = binding_context + self.values = values_delete + def test_copy_custom_certificate_deleting(self): + self.hook_run() + self.assertEqual(len(self.values[MODULE_NAME]["internal"].get("customCertificateData", {})), 0) + + diff --git a/hooks/lib/tests/test_internal_tls_test.py b/hooks/lib/tests/test_internal_tls_test.py new file mode 100644 index 00000000..de842cdd --- /dev/null +++ b/hooks/lib/tests/test_internal_tls_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lib.tests import testing +from lib.hooks.internal_tls import GenerateCertificateHook, default_sans, TlsSecret +from lib.certificate import parse +import lib.utils as utils +from OpenSSL import crypto +from ipaddress import ip_address + +NAME = "test" +MODULE_NAME = NAME +NAMESPACE = NAME +SANS = [ + NAME, + f"{NAME}.{NAMESPACE}", + f"{NAME}.{NAMESPACE}.svc" +] + +hook_generate = GenerateCertificateHook( + TlsSecret( + name=NAME, + sansGenerator=default_sans(SANS), + values_path_prefix=f"{MODULE_NAME}.internal.dvcr.cert"), + module_name=MODULE_NAME, + cn=NAME, + namespace=NAMESPACE) + +hook_regenerate = GenerateCertificateHook( + TlsSecret( + name=NAME, + sansGenerator=default_sans(SANS), + values_path_prefix=f"{MODULE_NAME}.internal.dvcr.cert"), + module_name=MODULE_NAME, + cn=NAME, + namespace=NAMESPACE, + expire=0) + +binding_context = [ + { + "binding": "binding", + "snapshots": {} + } +] + +values = { + "global": { + "modules": { + "publicDomainTemplate": "example.com" + }, + "discovery": { + "clusterDomain": "cluster.local" + } + }, + MODULE_NAME: { + "internal": {} + } +} + +class TestCertificate(testing.TestHook): + secret_data = {} + sans_default = SANS + ["localhost", "127.0.0.1"] + + @staticmethod + def parse_certificate(crt: str) -> crypto.X509: + return parse.parse_certificate(utils.base64_decode(crt)) + + def check_data(self): + self.assertGreater(len(self.values[MODULE_NAME]["internal"].get("dvcr", {}).get("cert", {})), 0) + self.secret_data = self.values[MODULE_NAME]["internal"]["dvcr"]["cert"] + self.assertTrue(utils.is_base64(self.secret_data.get("ca", ""))) + self.assertTrue(utils.is_base64(self.secret_data.get("crt", ""))) + self.assertTrue(utils.is_base64(self.secret_data.get("key", ""))) + + def check_sans(self, crt: crypto.X509) -> bool: + sans_from_cert = parse.get_certificate_san(crt) + sans = [] + for san in self.sans_default: + try: + ip_address(san) + sans.append(f"IP Address:{san}") + except ValueError: + sans.append(f"DNS:{san}") + sans_from_cert.sort() + sans.sort() + self.assertEqual(sans_from_cert, sans) + + def verify_certificate(self, ca: crypto.X509, crt: crypto.X509) -> crypto.X509StoreContextError: + store = crypto.X509Store() + store.add_cert(ca) + ctx = crypto.X509StoreContext(store, crt) + try: + ctx.verify_certificate() + return None + except crypto.X509StoreContextError as e: + return e + +class TestGenerateCertificate(TestCertificate): + def setUp(self): + self.func = hook_generate.reconcile() + self.bindind_context = binding_context + self.values = values + def test_generate_certificate(self): + self.hook_run() + self.check_data() + ca = self.parse_certificate(self.secret_data["ca"]) + crt = self.parse_certificate(self.secret_data["crt"]) + if (e := self.verify_certificate(ca, crt)) is not None: + self.fail(f"Certificate is not verify. Raised an exception: {e} ") + self.check_sans(crt) + +class TestReGenerateCertificate(TestCertificate): + def setUp(self): + self.func = hook_regenerate.reconcile() + self.bindind_context = binding_context + self.values = values + self.hook_run() + self.bindind_context[0]["snapshots"] = { + hook_regenerate.SNAPSHOT_SECRETS_NAME : [ + { + "filterResult": { + "data": { + "ca.crt" : self.values[MODULE_NAME]["internal"]["dvcr"]["cert"]["ca"], + "tls.crt": self.values[MODULE_NAME]["internal"]["dvcr"]["cert"]["crt"], + "key.crt": self.values[MODULE_NAME]["internal"]["dvcr"]["cert"]["key"] + }, + "name": NAME + } + } + ] + } + self.func = hook_generate.reconcile() + + def test_regenerate_certificate(self): + self.check_data() + ca = self.parse_certificate(self.secret_data["ca"]) + crt = self.parse_certificate(self.secret_data["crt"]) + if self.verify_certificate(ca, crt) is None: + self.fail(f"certificate has not expired") + self.hook_run() + self.check_data() + ca = self.parse_certificate(self.secret_data["ca"]) + crt = self.parse_certificate(self.secret_data["crt"]) + if (e := self.verify_certificate(ca, crt)) is not None: + self.fail(f"Certificate is not verify. Raised an exception: {e} ") + self.check_sans(crt) diff --git a/hooks/lib/tests/test_manage_tenant_secrets.py b/hooks/lib/tests/test_manage_tenant_secrets.py new file mode 100644 index 00000000..8e770599 --- /dev/null +++ b/hooks/lib/tests/test_manage_tenant_secrets.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lib.tests import testing +from lib.hooks.manage_tenant_secrets import ManageTenantSecretsHook + +hook = ManageTenantSecretsHook(source_namespace="source_namespace", + source_secret_name="secret_name", + pod_labels_to_follow={"app": "test"}, + destination_secret_labels={"test":"test"}, + module_name="test") + +binding_context = [ + { + "binding": "binding", + "snapshots": { + hook.POD_SNAPSHOT_NAME: [ + { + "filterResult": { + "namespace": "pod-namespace1" ## Create secret + } + }, + { + "filterResult": { + "namespace": "pod-namespace2" ## Don't create secret, because ns has deletionTimestamp + } + } + ], + hook.SECRETS_SNAPSHOT_NAME: [ + { + "filterResult": { + "data": {"test": "test"}, + "namespace": "source_namespace", + "type": "Opaque" + } + }, + { + "filterResult": { + "data": {"test": "test"}, + "namespace": "pod-namespace3", ## Delete secret, because namespace pod-namespace3 hasn't pods + "type": "Opaque" + } + }, + ], + hook.NAMESPACE_SNAPSHOT_NAME: [ + { + "filterResult": { + "name": "source_namespace", + "isTerminating": False + } + }, + { + "filterResult": { + "name": "pod-namespace1", + "isTerminating": False + } + }, + { + "filterResult": { + "name": "pod-namespace2", + "isTerminating": True + } + }, + { + "filterResult": { + "name": "pod-namespace3", + "isTerminating": False + } + }, + ] + } + } +] + +class TestManageSecrets(testing.TestHook): + def setUp(self): + self.func = hook.reconcile() + self.bindind_context = binding_context + self.values = {} + def test_manage_secrets(self): + self.hook_run() + self.assertEqual(len(self.kube_resources), 1) + self.assertEqual(self.kube_resources[0]["kind"], "Secret") + self.assertEqual(self.kube_resources[0]["metadata"]["name"], "secret_name") + self.assertEqual(self.kube_resources[0]["metadata"]["namespace"], "pod-namespace1") + self.assertEqual(self.kube_resources[0]["type"], "Opaque") + self.assertEqual(self.kube_resources[0]["data"], {'test': 'test'}) + self.assertEqual(self.kube_resources[0]["metadata"]["labels"], {'test': 'test'}) + diff --git a/hooks/lib/tests/testing.py b/hooks/lib/tests/testing.py new file mode 100644 index 00000000..f0257aa2 --- /dev/null +++ b/hooks/lib/tests/testing.py @@ -0,0 +1,57 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from deckhouse import hook +import unittest +import jsonpatch +import kubernetes_validate +import jsonschema + +class TestHook(unittest.TestCase): + kube_resources = [] + kube_version = "1.28" + def setUp(self): + self.bindind_context = [] + self.values = {} + self.func = None + + def tearDown(self): + pass + + def hook_run(self, validate_kube_resources: bool = True) -> None: + out = hook.testrun(func=self.func, + binding_context=self.bindind_context, + initial_values=self.values) + for patch in out.values_patches.data: + self.values = jsonpatch.apply_patch(self.values, [patch]) + + deletes = ("Delete", "DeleteInBackground", "DeleteNonCascading") + for kube_operation in out.kube_operations.data: + if kube_operation["operation"] in deletes: + continue + obj = kube_operation["object"] + if validate_kube_resources: + try: + ## TODO Validate CRD + kubernetes_validate.validate(obj, self.kube_version, strict=True) + self.kube_resources.append(obj) + except (kubernetes_validate.SchemaNotFoundError, + kubernetes_validate.InvalidSchemaError, + kubernetes_validate.ValidationError, + jsonschema.RefResolutionError) as e: + self.fail(f"Object is not valid. Raised an exception: {e} ") + else: + self.kube_resources.append(obj) + diff --git a/hooks/lib/utils.py b/hooks/lib/utils.py new file mode 100644 index 00000000..a486efd5 --- /dev/null +++ b/hooks/lib/utils.py @@ -0,0 +1,54 @@ +# +# Copyright 2023 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import os +import json + +def base64_encode(b: bytes) -> str: + return str(base64.b64encode(b), encoding='utf-8') + +def base64_decode(s: str) -> str: + return str(base64.b64decode(s), encoding="utf-8") + +def base64_encode_from_str(s: str) -> str: + return base64_encode(bytes(s, 'utf-8')) + +def json_load(path: str): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data + +def get_dir_path() -> str: + return os.path.dirname(os.path.abspath(__file__)) + +def is_base64(s): + try: + base64_decode(s) + return True + except base64.binascii.Error: + return False + +def check_elem_in_list(l: list, elem) -> bool: + for i in l: + if i == elem: + return True + return False + +def find_index_in_list(l: list, elem) -> int: + for i in range(len(l)): + if l[i] == elem: + return i + return \ No newline at end of file diff --git a/images/sds-utils-installer/Dockerfile b/images/sds-utils-installer/Dockerfile index a1e75877..9b84331d 100644 --- a/images/sds-utils-installer/Dockerfile +++ b/images/sds-utils-installer/Dockerfile @@ -49,7 +49,7 @@ FROM --platform=linux/amd64 $BASE_IMAGE WORKDIR / COPY --from=lvm-builder /lvm2/tools/lvm.static /sds-utils/bin/lvm.static COPY --from=bin-copier-builder /bin-copier /bin-copier - +COPY thin-pool-loader.sh . ENTRYPOINT ["/bin-copier"] CMD ["/sds-utils", "/opt/deckhouse/sds"] diff --git a/images/sds-utils-installer/thin-pool-loader.sh b/images/sds-utils-installer/thin-pool-loader.sh new file mode 100755 index 00000000..1577a5c2 --- /dev/null +++ b/images/sds-utils-installer/thin-pool-loader.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +thinPoolState=`nsenter lsmod | grep -E '^dm_thin_pool' | wc -l` +echo "Checking dm_thin_pool module presence" +if [ $thinPoolState -ne 1 ] +then + echo "Loading dm_thin_pool" + nsenter modprobe dm_thin_pool +fi diff --git a/images/webhooks/src/funcs/init.go b/images/webhooks/src/funcs/init.go new file mode 100644 index 00000000..fb6bc5b7 --- /dev/null +++ b/images/webhooks/src/funcs/init.go @@ -0,0 +1,51 @@ +package funcs + +import ( + "github.com/deckhouse/deckhouse/deckhouse-controller/pkg/apis/deckhouse.io/v1alpha1" + v1 "k8s.io/api/core/v1" + sv1 "k8s.io/api/storage/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewKubeClient() (client.Client, error) { + var config *rest.Config + var err error + + config, err = rest.InClusterConfig() + if err != nil { + klog.Fatal(err.Error()) + } + + if err != nil { + return nil, err + } + + var ( + resourcesSchemeFuncs = []func(*apiruntime.Scheme) error{ + v1alpha1.AddToScheme, + clientgoscheme.AddToScheme, + extv1.AddToScheme, + v1.AddToScheme, + sv1.AddToScheme, + } + ) + + scheme := apiruntime.NewScheme() + for _, f := range resourcesSchemeFuncs { + err = f(scheme) + if err != nil { + return nil, err + } + } + + clientOpts := client.Options{ + Scheme: scheme, + } + + return client.New(config, clientOpts) +} diff --git a/images/webhooks/src/go.mod b/images/webhooks/src/go.mod new file mode 100644 index 00000000..baac44d5 --- /dev/null +++ b/images/webhooks/src/go.mod @@ -0,0 +1,62 @@ +module webhooks + +go 1.22.0 + +toolchain go1.22.2 + +replace ( + github.com/deckhouse/deckhouse/dhctl v0.0.0 => github.com/deckhouse/deckhouse/dhctl v0.0.0-20240417135927-89f753e5e848 + github.com/deckhouse/deckhouse/go_lib/cloud-data v0.0.0 => github.com/deckhouse/deckhouse/go_lib/cloud-data v0.0.0-20240417135927-89f753e5e848 + go.cypherpunks.ru/gogost/v5 v5.13.0 => github.com/flant/gogost/v5 v5.13.0 +) + +require ( + github.com/deckhouse/deckhouse v1.59.4 + github.com/go-logr/logr v1.4.1 + k8s.io/api v0.30.0 + k8s.io/apiextensions-apiserver v0.29.2 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 + k8s.io/klog/v2 v2.120.1 + sigs.k8s.io/controller-runtime v0.17.3 +) + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/images/webhooks/src/go.sum b/images/webhooks/src/go.sum new file mode 100644 index 00000000..36b2e5d6 --- /dev/null +++ b/images/webhooks/src/go.sum @@ -0,0 +1,175 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/deckhouse v1.59.4 h1:PXDmNASATaV/QMUpCrjqadywOiPXdKSi2SJaeUU74OA= +github.com/deckhouse/deckhouse v1.59.4/go.mod h1:B8sCOATulfmtC3YrTnIg3PtxJk7SnmE4oTyCCzktO9Y= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= +k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= +sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/images/webhooks/src/main.go b/images/webhooks/src/main.go new file mode 100644 index 00000000..50852cd0 --- /dev/null +++ b/images/webhooks/src/main.go @@ -0,0 +1,45 @@ +/* +Copyright 2023 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "net/http" + "webhooks/validators" + + "k8s.io/klog/v2" +) + +var ( + tlscert, tlskey string +) + +const ( + port = ":8443" +) + +func main() { + flag.StringVar(&tlscert, "tlsCertFile", "/etc/certs/tls.crt", + "File containing a certificate for HTTPS.") + flag.StringVar(&tlskey, "tlsKeyFile", "/etc/certs/tls.key", + "File containing a private key for HTTPS.") + flag.Parse() + + http.HandleFunc("/mc-validate", validators.MCValidate) + + klog.Fatal(http.ListenAndServeTLS(port, tlscert, tlskey, nil)) +} diff --git a/images/webhooks/src/validators/mcValidator.go b/images/webhooks/src/validators/mcValidator.go new file mode 100644 index 00000000..200c3cc1 --- /dev/null +++ b/images/webhooks/src/validators/mcValidator.go @@ -0,0 +1,110 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validators + +import ( + "context" + "encoding/json" + dhctl "github.com/deckhouse/deckhouse/deckhouse-controller/pkg/apis/deckhouse.io/v1alpha1" + "github.com/go-logr/logr" + "k8s.io/api/admission/v1beta1" + "k8s.io/klog/v2" + "net/http" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "webhooks/funcs" +) + +type patchBoolValue struct { + Op string `json:"op"` + Path string `json:"path"` + Value bool `json:"value"` +} + +type mc struct { + Spec mcSpec `json:"spec"` + Metadata Metadata `json:"metadata"` +} + +type Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` +} + +type mcSpec struct { + Settings mcSettings `json:"settings"` +} + +type mcSettings struct { + EnableThinProvisioning bool `json:"enableThinProvisioning"` +} + +func MCValidate(w http.ResponseWriter, r *http.Request) { + logf.SetLogger(logr.Logger{}) + + arReview := v1beta1.AdmissionReview{} + if err := json.NewDecoder(r.Body).Decode(&arReview); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } else if arReview.Request == nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + raw := arReview.Request.Object.Raw + + arReview.Response = &v1beta1.AdmissionResponse{ + UID: arReview.Request.UID, + Allowed: true, + } + + mcJson := mc{} + klog.Infof("Retrieving MC object %v", json.Unmarshal(raw, &mcJson)) + + if (mcJson.Metadata.Name == "sds-replicated-volume" || mcJson.Metadata.Name == "sds-local-volume") && + mcJson.Spec.Settings.EnableThinProvisioning == true { + klog.Infof("in module %s enabled thin provisioning", mcJson.Metadata.Name) + + ctx := context.Background() + cl, err := funcs.NewKubeClient() + + if err != nil { + klog.Fatal(err.Error()) + } + + objs := dhctl.ModuleConfigList{} + + err = cl.List(ctx, &objs) + + for _, obj := range objs.Items { + if obj.Name == "sds-node-configurator" { + if obj.Spec.Settings == nil { + obj.Spec.Settings = map[string]interface{}{"enableThinProvisioning": true} + } else { + obj.Spec.Settings["enableThinProvisioning"] = true + } + err = cl.Update(ctx, &obj) + + if err != nil { + klog.Fatal(err.Error()) + } + } + } + } + + w.Header().Set("Content-Type", "application/json") + klog.Infof("Returning answer %v", json.NewEncoder(w).Encode(&arReview)) +} diff --git a/images/webhooks/werf.inc.yaml b/images/webhooks/werf.inc.yaml new file mode 100644 index 00000000..e5ade180 --- /dev/null +++ b/images/webhooks/werf.inc.yaml @@ -0,0 +1,16 @@ +--- +image: webhooks +from: "registry.deckhouse.io/base_images/golang:1.22.1-alpine@sha256:0de6cf7cceab6ecbf0718bdfb675b08b78113c3709c5e4b99456cdb2ae8c2495" + +git: + - add: /images/webhooks/src + to: /src + stageDependencies: + setup: + - "**/*" + +shell: + setup: + - cd /src + - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o webhooks + - mv webhooks /webhooks \ No newline at end of file diff --git a/openapi/config-values.yaml b/openapi/config-values.yaml index 3dd9a2cf..ba09a8c7 100644 --- a/openapi/config-values.yaml +++ b/openapi/config-values.yaml @@ -19,3 +19,7 @@ properties: - TRACE description: Module log level default: INFO + enableThinProvisioning: + type: boolean + default: false + description: Allow thin LVM volumes usage diff --git a/openapi/values.yaml b/openapi/values.yaml index bd4d77b4..9822bdbb 100644 --- a/openapi/values.yaml +++ b/openapi/values.yaml @@ -8,3 +8,20 @@ properties: properties: devChannel: type: string + customWebhookCert: + type: object + default: {} + x-required-for-helm: + - crt + - key + - ca + properties: + crt: + type: string + x-examples: ["YjY0ZW5jX3N0cmluZwo="] + key: + type: string + x-examples: ["YjY0ZW5jX3N0cmluZwo="] + ca: + type: string + x-examples: ["YjY0ZW5jX3N0cmluZwo="] diff --git a/templates/agent/daemonset.yaml b/templates/agent/daemonset.yaml index 970ddeb8..ab232b3d 100644 --- a/templates/agent/daemonset.yaml +++ b/templates/agent/daemonset.yaml @@ -63,6 +63,19 @@ spec: {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} {{- include "sds_utils_installer_resources" . | nindent 14 }} +{{- end }} +{{- if .Values.sdsNodeConfigurator.enableThinProvisioning }} + - name: thin-volumes-enabler + image: {{ include "helm_lib_module_image" (list . "sdsUtilsInstaller") }} + imagePullPolicy: IfNotPresent + command: + - /thin-pool-loader.sh + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} +{{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "static_utils_copier_resources" . | nindent 14 }} +{{- end }} {{- end }} containers: - name: sds-node-configurator-agent diff --git a/templates/webhooks/deployment.yaml b/templates/webhooks/deployment.yaml new file mode 100644 index 00000000..3d138948 --- /dev/null +++ b/templates/webhooks/deployment.yaml @@ -0,0 +1,78 @@ +{{- define "webhooks_resources" }} +cpu: 10m +memory: 50Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: webhooks + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: webhooks + updatePolicy: + updateMode: "Auto" + resourcePolicy: + containerPolicies: + - containerName: webhooks + minAllowed: + {{- include "webhooks_resources" . | nindent 8 }} + maxAllowed: + cpu: 20m + memory: 100Mi +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webhooks + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks" )) | nindent 2 }} +spec: + {{- include "helm_lib_deployment_on_master_strategy_and_replicas_for_ha" . | nindent 2 }} + selector: + matchLabels: + app: webhooks + template: + metadata: + labels: + app: webhooks + spec: + {{- include "helm_lib_priority_class" (tuple . "system-cluster-critical") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "any-node" "with-uninitialized" "with-cloud-provider-uninitialized") | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "master") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "webhooks")) | nindent 6 }} + containers: + - name: webhooks + command: + - /webhooks + image: {{ include "helm_lib_module_image" (list . "webhooks") }} + imagePullPolicy: IfNotPresent + volumeMounts: + - name: certs + mountPath: "/etc/certs" + readOnly: true + ports: + - name: http + containerPort: 8443 + protocol: TCP + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 12 }} +{{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "webhooks_resources" . | nindent 12 }} +{{- end }} + imagePullSecrets: + - name: {{ .Chart.Name }}-module-registry + serviceAccount: webhooks + serviceAccountName: webhooks + volumes: + - name: certs + secret: + secretName: webhooks-https-certs \ No newline at end of file diff --git a/templates/webhooks/rbac-for-us.yaml b/templates/webhooks/rbac-for-us.yaml new file mode 100644 index 00000000..7486dbd2 --- /dev/null +++ b/templates/webhooks/rbac-for-us.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webhooks + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks")) | nindent 2 }} +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: d8:{{ .Chart.Name }}:webhooks + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks")) | nindent 2 }} +rules: + - verbs: + - get + - list + - update + - patch + apiGroups: + - "deckhouse.io" + resources: + - "moduleconfigs" +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: d8:{{ .Chart.Name }}:webhooks + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks")) | nindent 2 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:webhooks +subjects: + - kind: ServiceAccount + name: webhooks + namespace: d8-{{ .Chart.Name }} diff --git a/templates/webhooks/secret.yaml b/templates/webhooks/secret.yaml new file mode 100644 index 00000000..55d975ea --- /dev/null +++ b/templates/webhooks/secret.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: webhooks-https-certs + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks")) | nindent 2 }} +type: kubernetes.io/tls +data: + ca.crt: {{ .Values.sdsNodeConfigurator.internal.customWebhookCert.ca }} + tls.crt: {{ .Values.sdsNodeConfigurator.internal.customWebhookCert.crt }} + tls.key: {{ .Values.sdsNodeConfigurator.internal.customWebhookCert.key }} diff --git a/templates/webhooks/service.yaml b/templates/webhooks/service.yaml new file mode 100644 index 00000000..dc01ddbf --- /dev/null +++ b/templates/webhooks/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: webhooks + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "webhooks" )) | nindent 2 }} +spec: + type: ClusterIP + ports: + - port: 443 + targetPort: 8443 + protocol: TCP + name: http + selector: + app: webhooks \ No newline at end of file diff --git a/templates/webhooks/webhook.yaml b/templates/webhooks/webhook.yaml new file mode 100644 index 00000000..045ba92f --- /dev/null +++ b/templates/webhooks/webhook.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: "d8-{{ .Chart.Name }}-mc-validation" +webhooks: + - name: "d8-{{ .Chart.Name }}-mc-validation.storage.deckhouse.io" + rules: + - apiGroups: ["deckhouse.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["moduleconfigs"] + scope: "Cluster" + clientConfig: + service: + namespace: "d8-{{ .Chart.Name }}" + name: "webhooks" + path: "/mc-validate" + caBundle: | + {{ .Values.sdsNodeConfigurator.internal.customWebhookCert.ca }} + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + timeoutSeconds: 5