Skip to content

Commit

Permalink
Merge pull request #157 from tr-aheiev/fix-avoidnamespaces-changes
Browse files Browse the repository at this point in the history
  • Loading branch information
axel7083 authored Feb 7, 2025
2 parents 7b3c638 + 3e1fadc commit 676e6a0
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 31 deletions.
2 changes: 1 addition & 1 deletion charts/cluster-secret/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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 @@ -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:
"""
Expand All @@ -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:
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 All @@ -188,7 +230,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,22 +50,23 @@ 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,
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 @@ -80,7 +81,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 @@ -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', [])

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 676e6a0

Please sign in to comment.