Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for 'avoidNamespaces' field updates #157

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions conformance/k8s_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -179,6 +180,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:
"""

Expand All @@ -190,37 +192,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:
Expand Down
44 changes: 43 additions & 1 deletion conformance/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=="
Expand Down Expand Up @@ -189,7 +231,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.'
)
Expand Down
21 changes: 9 additions & 12 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,24 @@ def on_delete(
logger.debug(f'csec {uid} deleted from memory ok')


@kopf.on.field('clustersecret.io', 'v1', 'clustersecrets', field='avoidNamespaces')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could have a conformance test ensuring this change work as expected ?

Copy link
Author

@tr-aheiev tr-aheiev Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could have a conformance test ensuring this change work as expected ?

Yes, we can have some test. I will check current tests and try to add something there.

@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,
namespace: str,
body,
uid: str,
logger: logging.Logger,
reason: kopf.Reason,
**_,
):
logger.debug(f'Namespaces changed: {old} -> {new}')

if old is None:
if reason == "create":
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', [])
Expand All @@ -81,7 +82,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:
Expand Down Expand Up @@ -116,13 +117,14 @@ def on_field_data(
namespace: Optional[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', [])

Expand Down Expand Up @@ -202,11 +204,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,
Expand Down