From 746e50b465802eb4628025a24857b99c7dd6d4a5 Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Sun, 19 Jan 2025 04:46:52 +0200 Subject: [PATCH 1/6] Added event handler for changes in 'avoidNamespaces' field --- src/handlers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handlers.py b/src/handlers.py index a32fbff..c6eae09 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -50,8 +50,9 @@ def on_delete( logger.debug(f'csec {uid} deleted from memory ok') +@kopf.on.field('clustersecret.io', 'v1', 'clustersecrets', field='avoidNamespaces') @kopf.on.field('clustersecret.io', 'v1', 'clustersecrets', field='matchNamespace') -def on_field_match_namespace( +def on_fields_avoid_or_match_namespace( old: Optional[List[str]], new: List[str], name: str, @@ -60,12 +61,11 @@ def on_field_match_namespace( logger: logging.Logger, **_, ): - logger.debug(f'Namespaces changed: {old} -> {new}') - if old is None: logger.debug('This is a new object: Ignoring.') return + logger.debug(f'Avoid or match namespaces changed: {old} -> {new}') logger.debug(f'Updating Object body == {body}') syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', []) @@ -80,7 +80,7 @@ def on_field_match_namespace( sync_secret(logger, secret_namespace, body, v1) for secret_namespace in to_remove: - delete_secret(logger, secret_namespace, name, v1=v1) + delete_secret(logger, secret_namespace, name, v1) cached_cluster_secret = csecs_cache.get_cluster_secret(uid) if cached_cluster_secret is None: From de0ab7a507fbeb1b259eeb12e4cd3f4959e41b0a Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Wed, 22 Jan 2025 00:47:52 +0200 Subject: [PATCH 2/6] Fixed correct event handling when fields 'matchNamespace' or 'avoidNamespaces' was missing before they updates Removed unnecessary cache checking - it always will be empty for 'resume' and 'create' events --- src/handlers.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/handlers.py b/src/handlers.py index c6eae09..103a396 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -59,9 +59,10 @@ def on_fields_avoid_or_match_namespace( body, uid: str, logger: logging.Logger, + reason: kopf.Reason, **_, ): - if old is None: + if reason == "create": logger.debug('This is a new object: Ignoring.') return @@ -113,13 +114,14 @@ def on_field_data( name: str, uid: str, logger: logging.Logger, + reason: kopf.Reason, **_, ): - logger.debug(f'Data changed: {old} -> {new}') - if old is None: + if reason == "create": logger.debug('This is a new object: Ignoring') return + logger.debug(f'Data changed: {old} -> {new}') logger.debug(f'Updating Object body == {body}') syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', []) @@ -197,11 +199,6 @@ async def create_fn( for ns in matchedns: sync_secret(logger, ns, body, v1) - # store status in memory - cached_cluster_secret = csecs_cache.get_cluster_secret(uid) - if cached_cluster_secret is None: - logger.error('Received an event for an unknown ClusterSecret.') - # Updating the cache csecs_cache.set_cluster_secret(BaseClusterSecret( uid=uid, From ce796ab6802196e4e827cf9c2a33b5a9f309becd Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Wed, 22 Jan 2025 00:52:14 +0200 Subject: [PATCH 3/6] Refactored/improved tests validation function --- conformance/k8s_utils.py | 49 ++++++++++++++++++++++++++-------------- conformance/tests.py | 2 +- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/conformance/k8s_utils.py b/conformance/k8s_utils.py index 92f7dd4..83598a3 100644 --- a/conformance/k8s_utils.py +++ b/conformance/k8s_utils.py @@ -65,6 +65,7 @@ def __init__(self, custom_objects_api: CustomObjectsApi, api_instance: CoreV1Api # immutable after self.retry_attempts = 3 self.retry_delay = 5 + self.before_validate_delay = 3 def create_secret( self, @@ -178,6 +179,7 @@ def validate_namespace_secrets( namespaces: Optional[List[str]] = None, labels: Optional[Dict[str, str]] = None, annotations: Optional[Dict[str, str]] = None, + check_missing: Optional[bool] = False, ) -> bool: """ @@ -189,37 +191,50 @@ def validate_namespace_secrets( If None, it means the secret should be present in ALL namespaces annotations: Optional[Dict[str, str]] labels: Optional[Dict[str, str]] + check_missing: Optional[bool] + If True, it checks if the secret is missing in the namespaces Returns ------- """ - all_namespaces = [item.metadata.name for item in self.api_instance.list_namespace().items] - def validate() -> Optional[str]: - for namespace in all_namespaces: - - secret = self.get_kubernetes_secret(name=name, namespace=namespace) + if namespaces is None or len(namespaces) == 0: + # It parses all kubernetes namespaces and check each of them with 'validate_specific_secret' function. If none of them return an answer, it returns None. + all_namespaces = (ns.metadata.name for ns in self.api_instance.list_namespace().items) + return next((answer for namespace in all_namespaces if (answer := validate_specific_secret(namespace))), None) + elif len(namespaces) > 1: + # Do the same as previous block but with incoming (to function from outside) namespaces list. + return next((answer for namespace in namespaces if (answer := validate_specific_secret(namespace))), None) + else: + # If incoming namespace only one, check only that specific namespace. + return validate_specific_secret(namespaces[0]) - if namespaces is not None and namespace not in namespaces: - if secret is None: - continue - return f'' + def validate_specific_secret(namespace) -> Optional[str]: + secret = self.get_kubernetes_secret(name, namespace) - if secret is None: - return f'secret {name} is none in namespace {namespace}.' + if check_missing and secret is not None: + return f'secret {name} is present in namespace {namespace}.' + elif check_missing: + return None - if secret.data != data: - return f'secret {name} data mismatch in namespace {namespace}.' + if secret is None: + return f'secret {name} is none in namespace {namespace}.' - if annotations is not None and not is_subset(secret.metadata.annotations, annotations): - return f'secret {name} annotations mismatch in namespace {namespace}.' + if secret.data != data: + return f'secret {name} data mismatch in namespace {namespace}.' - if labels is not None and not is_subset(secret.metadata.labels, labels): - return f'secret {name} labels mismatch in namespace {namespace}.' + if annotations is not None and not is_subset(secret.metadata.annotations, annotations): + return f'secret {name} annotations mismatch in namespace {namespace}.' + if labels is not None and not is_subset(secret.metadata.labels, labels): + return f'secret {name} labels mismatch in namespace {namespace}.' + return None + # This is to wait before previous kubernetes operations are completed. + sleep(self.before_validate_delay) + return self.retry(validate) def retry(self, f: Callable[[], Optional[str]]) -> bool: diff --git a/conformance/tests.py b/conformance/tests.py index d58edd0..87dd27a 100644 --- a/conformance/tests.py +++ b/conformance/tests.py @@ -188,7 +188,7 @@ def test_simple_cluster_secret_deleted(self): self.cluster_secret_manager.validate_namespace_secrets( name=name, data={"username": username_data}, - namespaces=[], + check_missing=True, ), f'secret {name} should be deleted from all namespaces.' ) From 8ec9bf9f4c56f199d530b539d2ad2f9abf5e4874 Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Wed, 22 Jan 2025 00:53:11 +0200 Subject: [PATCH 4/6] Added test for 'avoidNamespaces' field changes --- conformance/tests.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/conformance/tests.py b/conformance/tests.py index 87dd27a..f63161b 100644 --- a/conformance/tests.py +++ b/conformance/tests.py @@ -162,6 +162,48 @@ def test_patch_cluster_secret_match_namespaces(self): f'secret {name} should be in all user namespaces' ) + def test_patch_cluster_secret_avoid_namespaces(self): + name = "dynamic-cluster-secret-avoid-namespaces" + username_data = "MTIzNDU2Cg==" + + # Create a secret in all user namespaces + self.cluster_secret_manager.create_cluster_secret( + name=name, + data={"username": username_data}, + match_namespace=USER_NAMESPACES + ) + + # We expect the secret to be in all user namespaces + self.assertTrue( + self.cluster_secret_manager.validate_namespace_secrets( + name=name, + data={"username": username_data}, + namespaces=USER_NAMESPACES, + ) + ) + + # Update the cluster avoid_namespaces to exclude second namespace + self.cluster_secret_manager.update_data_cluster_secret( + name=name, + data={"username": username_data}, + match_namespace=USER_NAMESPACES, + avoid_namespaces=[ + USER_NAMESPACES[1] + ], + ) + + self.assertTrue( + self.cluster_secret_manager.validate_namespace_secrets( + name=name, + data={"username": username_data}, + namespaces=[ + USER_NAMESPACES[1] + ], + check_missing=True, + ), + f'secret {name} should be deleted from second namespace' + ) + def test_simple_cluster_secret_deleted(self): name = "simple-cluster-secret-deleted" username_data = "MTIzNDU2Cg==" From 0aa091bc271c91e58dbb3f5d82c96ed5ade43a68 Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Fri, 7 Feb 2025 00:16:45 +0200 Subject: [PATCH 5/6] Fixed unit tests after changes in 'on_field_data' handlers function --- src/tests/test_handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/test_handlers.py b/src/tests/test_handlers.py index 1a6f5b4..9092379 100644 --- a/src/tests/test_handlers.py +++ b/src/tests/test_handlers.py @@ -43,6 +43,7 @@ def test_on_field_data_cache(self): name="mysecret", uid="mysecretuid", logger=self.logger, + reason="update", ) # New data should be in the cache. @@ -99,6 +100,7 @@ def test_on_field_data_sync(self): name="mysecret", uid="mysecretuid", logger=self.logger, + reason="update", ) # Namespaced secret should be updated. @@ -226,6 +228,7 @@ def read_namespace(name, **kwargs): name="mysecret", uid="mysecretuid", logger=self.logger, + reason="update", ) # Namespaced secret should be updated with the new data. From 3e1fadc18c7e6b55b44929474d4797a20b3f612e Mon Sep 17 00:00:00 2001 From: tymuraheiev Date: Fri, 7 Feb 2025 00:17:49 +0200 Subject: [PATCH 6/6] Bump chart version to 0.5.2 --- charts/cluster-secret/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/cluster-secret/Chart.yaml b/charts/cluster-secret/Chart.yaml index fad391f..856bd57 100755 --- a/charts/cluster-secret/Chart.yaml +++ b/charts/cluster-secret/Chart.yaml @@ -3,7 +3,7 @@ name: cluster-secret description: ClusterSecret Operator kubeVersion: '>= 1.25.0-0' type: application -version: 0.5.1 +version: 0.5.2 icon: https://clustersecret.com/assets/csninjasmall.png sources: - https://github.com/zakkg3/ClusterSecret