diff --git a/hooks/common.py b/hooks/common.py new file mode 100644 index 00000000..4982840b --- /dev/null +++ b/hooks/common.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# 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. + +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/ensure_crds.py b/hooks/ensure_crds.py index 8fe8a0cd..304a8516 100755 --- a/hooks/ensure_crds.py +++ b/hooks/ensure_crds.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2023 Flant JSC +# 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. diff --git a/hooks/generate_webhook_certs.py b/hooks/generate_webhook_certs.py new file mode 100755 index 00000000..9df904fa --- /dev/null +++ b/hooks/generate_webhook_certs.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# 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. + + +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-health-watcher-controller/cmd/main.go b/images/sds-health-watcher-controller/cmd/main.go index 7a990daf..676387c8 100644 --- a/images/sds-health-watcher-controller/cmd/main.go +++ b/images/sds-health-watcher-controller/cmd/main.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + dh "github.com/deckhouse/deckhouse/deckhouse-controller/pkg/apis/deckhouse.io/v1alpha1" "github.com/deckhouse/sds-node-configurator/api/v1alpha1" "os" goruntime "runtime" @@ -40,6 +41,7 @@ import ( var ( resourcesSchemeFuncs = []func(*apiruntime.Scheme) error{ + dh.AddToScheme, v1alpha1.AddToScheme, clientgoscheme.AddToScheme, extv1.AddToScheme, @@ -116,6 +118,12 @@ func main() { os.Exit(1) } + err = controller.RunMCWatcher(mgr, *log) + if err != nil { + log.Error(err, "[main] unable to run MCWatcher controller") + os.Exit(1) + } + if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { log.Error(err, "[main] unable to mgr.AddHealthzCheck") os.Exit(1) diff --git a/images/sds-health-watcher-controller/go.mod b/images/sds-health-watcher-controller/go.mod index 64ad647e..d0c512d2 100644 --- a/images/sds-health-watcher-controller/go.mod +++ b/images/sds-health-watcher-controller/go.mod @@ -3,10 +3,12 @@ module sds-health-watcher-controller go 1.22.2 require ( + github.com/cloudflare/cfssl v1.5.0 + github.com/deckhouse/deckhouse v1.62.4 github.com/deckhouse/sds-node-configurator/api v0.0.0-20240709091744-c9d24f05db41 github.com/go-logr/logr v1.4.1 - github.com/prometheus/client_golang v1.18.0 - github.com/stretchr/testify v1.8.4 + github.com/prometheus/client_golang v1.19.0 + github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.30.1 k8s.io/apiextensions-apiserver v0.29.4 @@ -18,46 +20,46 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // 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 v4.12.0+incompatible // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.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/go-openapi/swag v0.22.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/google/uuid v1.6.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/matttproud/golang_protobuf_extensions/v2 v2.0.0 // 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/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // 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/oauth2 v0.17.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // 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 @@ -67,3 +69,22 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/deckhouse/deckhouse/dhctl => ./dhctl + +replace github.com/deckhouse/deckhouse/go_lib/cloud-data => ./go_lib/cloud-data + +replace github.com/deckhouse/deckhouse/egress-gateway-agent => ./ee/modules/021-cni-cilium/images/egress-gateway-agent + +// Remove 'in body' from errors, fix for Go 1.16 (https://github.com/go-openapi/validate/pull/138). +replace github.com/go-openapi/validate => github.com/flant/go-openapi-validate v0.19.12-flant.1 + +// replace with master branch to work with single dash +replace gopkg.in/alecthomas/kingpin.v2 => github.com/alecthomas/kingpin v1.3.8-0.20200323085623-b6657d9477a6 + +replace go.cypherpunks.ru/gogost/v5 v5.13.0 => github.com/flant/gogost/v5 v5.13.0 + +// swag v0.22+ breaks schemas_test.go:TestMapMergeAnchor, seems it doesn't support anchoring. Have to figure out that. +replace github.com/go-openapi/swag => github.com/go-openapi/swag v0.21.1 + +replace github.com/deckhouse/deckhouse/go_lib/registry-packages-proxy => ./go_lib/registry-packages-proxy diff --git a/images/sds-health-watcher-controller/go.sum b/images/sds-health-watcher-controller/go.sum index fb659454..f03da765 100644 --- a/images/sds-health-watcher-controller/go.sum +++ b/images/sds-health-watcher-controller/go.sum @@ -1,23 +1,37 @@ +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +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/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= +github.com/cloudflare/cfssl v1.5.0 h1:vFJDAvQgFSRbCn9zg8KpSrrEZrBAQ4KO5oNK7SXEyb0= +github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ= +github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= +github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 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/sds-node-configurator/api v0.0.0-20240704133537-7fc08e30741c h1:L7y2+Vr3VTgERytPdj6VwocJpUGR6KMh6WeBw4IW8to= -github.com/deckhouse/sds-node-configurator/api v0.0.0-20240704133537-7fc08e30741c/go.mod h1:H71+9G0Jr46Qs0BA3z3/xt0h9lbnJnCEYcaCJCWFBf0= +github.com/deckhouse/deckhouse v1.62.4 h1:Jgd9TSGLRE/0nYsg3KabMvguVp+d6oqt/GTxO7EHgO4= +github.com/deckhouse/deckhouse v1.62.4/go.mod h1:uJICbx/itedld6N9uv3srI6Hdt+m4P6IQyocUrtySVY= github.com/deckhouse/sds-node-configurator/api v0.0.0-20240709091744-c9d24f05db41 h1:kfnAfII4E8yWkDZ4FJIPO9/OvXkMIQDPLB3zzNBo8Wg= github.com/deckhouse/sds-node-configurator/api v0.0.0-20240709091744-c9d24f05db41/go.mod h1:H71+9G0Jr46Qs0BA3z3/xt0h9lbnJnCEYcaCJCWFBf0= 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 v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 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= @@ -26,8 +40,9 @@ github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn 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-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 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= @@ -35,10 +50,14 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= 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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= @@ -47,108 +66,163 @@ 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.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 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/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +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/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 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/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/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/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= 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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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= @@ -157,19 +231,26 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -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/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.0-20200615113413-eeeca48fe776/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.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= diff --git a/images/sds-health-watcher-controller/pkg/controller/lvg_conditions_watcher.go b/images/sds-health-watcher-controller/pkg/controller/lvg_conditions_watcher.go index 8b50009d..5e8f4501 100644 --- a/images/sds-health-watcher-controller/pkg/controller/lvg_conditions_watcher.go +++ b/images/sds-health-watcher-controller/pkg/controller/lvg_conditions_watcher.go @@ -104,12 +104,12 @@ func RunLVGConditionsWatcher( log.Info(fmt.Sprintf("[RunLVGConditionsWatcher] createFunc added a request for the LVMVolumeGroup %s to the Reconcilers queue", e.Object.GetName())) }, UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { - log.Info(fmt.Sprintf("[RunLVGConditionsWatcher] got a create event for the LVMVolumeGroup %s", e.ObjectNew.GetName())) + log.Info(fmt.Sprintf("[RunLVGConditionsWatcher] got a update event for the LVMVolumeGroup %s", e.ObjectNew.GetName())) oldLVG, ok := e.ObjectOld.(*v1alpha1.LvmVolumeGroup) if !ok { err = errors.New("unable to cast event object to a given type") - log.Error(err, "[RunLVGConditionsWatcher] an error occurred while handling a create event") + log.Error(err, "[RunLVGConditionsWatcher] an error occurred while handling a update event") return } log.Debug(fmt.Sprintf("[RunLVGConditionsWatcher] successfully casted an old state of the LVMVolumeGroup %s", oldLVG.Name)) @@ -117,7 +117,7 @@ func RunLVGConditionsWatcher( newLVG, ok := e.ObjectNew.(*v1alpha1.LvmVolumeGroup) if !ok { err = errors.New("unable to cast event object to a given type") - log.Error(err, "[RunLVGConditionsWatcher] an error occurred while handling a create event") + log.Error(err, "[RunLVGConditionsWatcher] an error occurred while handling a update event") return } log.Debug(fmt.Sprintf("[RunLVGConditionsWatcher] successfully casted a new state of the LVMVolumeGroup %s", newLVG.Name)) diff --git a/images/sds-health-watcher-controller/pkg/controller/mc_watcher.go b/images/sds-health-watcher-controller/pkg/controller/mc_watcher.go new file mode 100644 index 00000000..21c502a2 --- /dev/null +++ b/images/sds-health-watcher-controller/pkg/controller/mc_watcher.go @@ -0,0 +1,126 @@ +/* +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 controller + +import ( + "context" + "encoding/json" + "fmt" + "github.com/cloudflare/cfssl/log" + dh "github.com/deckhouse/deckhouse/deckhouse-controller/pkg/apis/deckhouse.io/v1alpha1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sds-health-watcher-controller/pkg/logger" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + MCWatcherCtrlName = "sds-mc-watcher-controller" + sdsNodeConfiguratorModuleName = "sds-node-configurator" + sdsLocalVolumeModuleName = "sds-local-volume" + sdsReplicatedVolumeName = "sds-replicated-volume" +) + +func RunMCWatcher( + mgr manager.Manager, + log logger.Logger, +) error { + cl := mgr.GetClient() + + c, err := controller.New(MCWatcherCtrlName, mgr, controller.Options{ + Reconciler: reconcile.Func(func(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + log.Info(fmt.Sprintf("[RunLVGConditionsWatcher] Reconciler got a request %s", request.String())) + return reconcile.Result{}, nil + }), + }) + + if err != nil { + log.Error(err, "[MCWatcherCtrlName] unable to create a controller") + return err + } + + err = c.Watch(source.Kind(mgr.GetCache(), &dh.ModuleConfig{}), handler.Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + log.Info(fmt.Sprintf("[MCWatcherCtrlName] got a create event for the ModuleConfig %s", e.Object.GetName())) + checkMCThinPoolsEnabled(ctx, cl) + }, + UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { + log.Info(fmt.Sprintf("[MCWatcherCtrlName] got a update event for the ModuleConfig %s", e.ObjectNew.GetName())) + checkMCThinPoolsEnabled(ctx, cl) + }, + }) + if err != nil { + log.Error(err, "[MCWatcherCtrlName] unable to watch the events") + return err + } + + return nil +} + +func checkMCThinPoolsEnabled(ctx context.Context, cl client.Client) { + listDevice := &dh.ModuleConfigList{} + + err := cl.List(ctx, listDevice) + if err != nil { + log.Fatal(err) + } + + for _, moduleItem := range listDevice.Items { + if moduleItem.Name != sdsLocalVolumeModuleName && moduleItem.Name != sdsReplicatedVolumeName { + continue + } + + if value, exists := moduleItem.Spec.Settings["enableThinProvisioning"]; exists && value == true { + ctx := context.Background() + + sncModuleConfig := &dh.ModuleConfig{} + + err = cl.Get(ctx, types.NamespacedName{Name: sdsNodeConfiguratorModuleName, Namespace: ""}, sncModuleConfig) + if err != nil { + log.Fatal(err) + } + + if value, exists := sncModuleConfig.Spec.Settings["enableThinProvisioning"]; exists && value == true { + log.Info("Thin pools support is enabled") + } else { + log.Info("Enabling thin pools support") + patchBytes, err := json.Marshal(map[string]interface{}{ + "spec": map[string]interface{}{ + "settings": map[string]interface{}{ + "enableThinProvisioning": true, + }, + }, + }) + + if err != nil { + log.Fatalf("Error marshalling patch: %s", err.Error()) + } + + err = cl.Patch(context.TODO(), sncModuleConfig, client.RawPatch(types.MergePatchType, patchBytes)) + if err != nil { + log.Fatalf("Error patching object: %s", err.Error()) + } + } + } + } +} 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/thin-pool-loader/src/go.mod b/images/thin-pool-loader/src/go.mod new file mode 100644 index 00000000..26a78852 --- /dev/null +++ b/images/thin-pool-loader/src/go.mod @@ -0,0 +1,7 @@ +module thin-pool-loader + +go 1.22.3 + +require k8s.io/klog/v2 v2.130.1 + +require github.com/go-logr/logr v1.4.1 // indirect diff --git a/images/thin-pool-loader/src/go.sum b/images/thin-pool-loader/src/go.sum new file mode 100644 index 00000000..d28ed98b --- /dev/null +++ b/images/thin-pool-loader/src/go.sum @@ -0,0 +1,4 @@ +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= diff --git a/images/thin-pool-loader/src/main.go b/images/thin-pool-loader/src/main.go new file mode 100644 index 00000000..c18297d2 --- /dev/null +++ b/images/thin-pool-loader/src/main.go @@ -0,0 +1,32 @@ +/* +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 main + +import ( + "k8s.io/klog/v2" + "os/exec" +) + +func main() { + klog.Info("Loading thin pool kernel module") + cmd := exec.Command("nsenter modprobe dm_thin_pool") + + err := cmd.Run() + if err != nil { + klog.Fatal(err) + } +} diff --git a/images/thin-pool-loader/werf.inc.yaml b/images/thin-pool-loader/werf.inc.yaml new file mode 100644 index 00000000..c74c01d3 --- /dev/null +++ b/images/thin-pool-loader/werf.inc.yaml @@ -0,0 +1,99 @@ +{{- $_ := set . "BASE_GOLANG_22_ALPINE" "registry.deckhouse.ru/base_images/golang:1.22.3-alpine@sha256:dbf216b880b802c22e3f4f2ef0a78396b4a9a6983cb9b767c5efc351ebf946b0" }} +{{- $_ := set . "BASE_SCRATCH" "registry.deckhouse.ru/base_images/scratch@sha256:b054705fcc9f2205777d80a558d920c0b4209efdc3163c22b5bfcb5dda1db5fc" }} +{{- $_ := set . "BASE_ALPINE_DEV" "registry.deckhouse.ru/base_images/dev-alpine:3.16.3@sha256:c706fa83cc129079e430480369a3f062b8178cac9ec89266ebab753a574aca8e" }} +{{- $_ := set . "BASE_ALT_DEV" "registry.deckhouse.ru/base_images/dev-alt:p10@sha256:76e6e163fa982f03468166203488b569e6d9fc10855d6a259c662706436cdcad" }} + +{{ $binaries := "/sds-utils/bin/lvm.static" }} +{{ $lvm_version := "d786a8f820d54ce87a919e6af5426c333c173b11" }} +--- +image: {{ $.ImageName }}-binaries-artifact +from: {{ $.BASE_ALT_DEV }} +final: false + +shell: + install: + - apt-get update + - | + apt-get install -y \ + build-essential \ + autoconf \ + automake \ + libtool \ + pkg-config \ + libdevmapper-devel \ + libaio-devel-static \ + libblkid-devel-static \ + thin-provisioning-tools \ + git + - cd / + - git clone {{ env "SOURCE_REPO" }}/lvmteam/lvm2.git + - cd /lvm2 + - git checkout {{ $lvm_version }} + - ./configure --enable-static_link --disable-silent-rules --disable-readline --enable-blkid_wiping --build=x86_64-linux-gnu + - make + - mkdir -p /sds-utils/bin/ + - mv /lvm2/tools/lvm.static /sds-utils/bin/lvm.static + - /binary_replace.sh -i "{{ $binaries }}" -o /relocate + +--- +image: {{ $.ImageName }}-golang-artifact +from: {{ $.BASE_GOLANG_22_ALPINE }} +final: false + +git: + - add: /images/thin-pool-loader/src + to: /src + stageDependencies: + setup: + - "**/*" + +shell: + setup: + - cd /src + - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o thin-pool-loader + - mv thin-pool-loader /thin-pool-loader + +--- +image: {{ $.ImageName }}-distroless-artifact +from: {{ $.BASE_ALPINE_DEV }} +final: false + +shell: + install: + - mkdir -p /relocate/bin /relocate/sbin /relocate/etc /relocate/etc/ssl /relocate/usr/bin /relocate/usr/sbin /relocate/usr/share + - cp -pr /tmp /relocate + - cp -pr /etc/passwd /etc/group /etc/hostname /etc/hosts /etc/shadow /etc/protocols /etc/services /etc/nsswitch.conf /relocate/etc + - cp -pr /usr/share/ca-certificates /relocate/usr/share + - cp -pr /usr/share/zoneinfo /relocate/usr/share + - cp -pr etc/ssl/cert.pem /relocate/etc/ssl + - cp -pr /etc/ssl/certs /relocate/etc/ssl + - echo "deckhouse:x:64535:64535:deckhouse:/:/sbin/nologin" >> /relocate/etc/passwd + - echo "deckhouse:x:64535:" >> /relocate/etc/group + - echo "deckhouse:!::0:::::" >> /relocate/etc/shadow + +--- +image: {{ $.ImageName }}-distroless +from: {{ $.BASE_SCRATCH }} +final: false + +import: + - image: {{ $.ImageName }}-distroless-artifact + add: /relocate + to: / + before: setup + +--- +image: {{ $.ImageName }} +fromImage: {{ $.ImageName }}-distroless +import: + - image: {{ $.ImageName }}-binaries-artifact + add: /relocate + to: / + before: setup + - image: {{ $.ImageName }}-golang-artifact + add: /thin-pool-loader + to: /thin-pool-loader + before: setup + +docker: + ENTRYPOINT: ["/thin-pool-loader"] diff --git a/openapi/config-values.yaml b/openapi/config-values.yaml index 6da1fec3..24c959cd 100644 --- a/openapi/config-values.yaml +++ b/openapi/config-values.yaml @@ -16,3 +16,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..d1d8cb40 100644 --- a/openapi/values.yaml +++ b/openapi/values.yaml @@ -6,5 +6,28 @@ properties: type: object default: {} properties: - devChannel: - type: string + pythonVersions: + type: array + default: [] + items: + 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="] + registry: + type: object + description: "System field, overwritten by Deckhouse. Don't use" diff --git a/templates/agent/daemonset.yaml b/templates/agent/daemonset.yaml index 897066e4..d6708db2 100644 --- a/templates/agent/daemonset.yaml +++ b/templates/agent/daemonset.yaml @@ -64,6 +64,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 . "thinPoolLoader") }} + imagePullPolicy: IfNotPresent + command: + - /thin-pool-loader + 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/sds-health-watcher-controller/rbac-for-us.yaml b/templates/sds-health-watcher-controller/rbac-for-us.yaml index 3e702c64..967d2720 100644 --- a/templates/sds-health-watcher-controller/rbac-for-us.yaml +++ b/templates/sds-health-watcher-controller/rbac-for-us.yaml @@ -52,6 +52,16 @@ rules: - delete - update - create + - verbs: + - get + - list + - update + - patch + - watch + apiGroups: + - "deckhouse.io" + resources: + - "moduleconfigs" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding