Skip to content

Commit 676e6a0

Browse files
authored
Merge pull request #157 from tr-aheiev/fix-avoidnamespaces-changes
2 parents 7b3c638 + 3e1fadc commit 676e6a0

File tree

5 files changed

+88
-31
lines changed

5 files changed

+88
-31
lines changed

charts/cluster-secret/Chart.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: cluster-secret
33
description: ClusterSecret Operator
44
kubeVersion: '>= 1.25.0-0'
55
type: application
6-
version: 0.5.1
6+
version: 0.5.2
77
icon: https://clustersecret.com/assets/csninjasmall.png
88
sources:
99
- https://github.com/zakkg3/ClusterSecret

conformance/k8s_utils.py

+32-17
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__(self, custom_objects_api: CustomObjectsApi, api_instance: CoreV1Api
6565
# immutable after
6666
self.retry_attempts = 3
6767
self.retry_delay = 5
68+
self.before_validate_delay = 3
6869

6970
def create_secret(
7071
self,
@@ -178,6 +179,7 @@ def validate_namespace_secrets(
178179
namespaces: Optional[List[str]] = None,
179180
labels: Optional[Dict[str, str]] = None,
180181
annotations: Optional[Dict[str, str]] = None,
182+
check_missing: Optional[bool] = False,
181183
) -> bool:
182184
"""
183185
@@ -189,37 +191,50 @@ def validate_namespace_secrets(
189191
If None, it means the secret should be present in ALL namespaces
190192
annotations: Optional[Dict[str, str]]
191193
labels: Optional[Dict[str, str]]
194+
check_missing: Optional[bool]
195+
If True, it checks if the secret is missing in the namespaces
192196
193197
Returns
194198
-------
195199
196200
"""
197-
all_namespaces = [item.metadata.name for item in self.api_instance.list_namespace().items]
198-
199201
def validate() -> Optional[str]:
200-
for namespace in all_namespaces:
201-
202-
secret = self.get_kubernetes_secret(name=name, namespace=namespace)
202+
if namespaces is None or len(namespaces) == 0:
203+
# 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.
204+
all_namespaces = (ns.metadata.name for ns in self.api_instance.list_namespace().items)
205+
return next((answer for namespace in all_namespaces if (answer := validate_specific_secret(namespace))), None)
206+
elif len(namespaces) > 1:
207+
# Do the same as previous block but with incoming (to function from outside) namespaces list.
208+
return next((answer for namespace in namespaces if (answer := validate_specific_secret(namespace))), None)
209+
else:
210+
# If incoming namespace only one, check only that specific namespace.
211+
return validate_specific_secret(namespaces[0])
203212

204-
if namespaces is not None and namespace not in namespaces:
205-
if secret is None:
206-
continue
207-
return f''
213+
def validate_specific_secret(namespace) -> Optional[str]:
214+
secret = self.get_kubernetes_secret(name, namespace)
208215

209-
if secret is None:
210-
return f'secret {name} is none in namespace {namespace}.'
216+
if check_missing and secret is not None:
217+
return f'secret {name} is present in namespace {namespace}.'
218+
elif check_missing:
219+
return None
211220

212-
if secret.data != data:
213-
return f'secret {name} data mismatch in namespace {namespace}.'
221+
if secret is None:
222+
return f'secret {name} is none in namespace {namespace}.'
214223

215-
if annotations is not None and not is_subset(secret.metadata.annotations, annotations):
216-
return f'secret {name} annotations mismatch in namespace {namespace}.'
224+
if secret.data != data:
225+
return f'secret {name} data mismatch in namespace {namespace}.'
217226

218-
if labels is not None and not is_subset(secret.metadata.labels, labels):
219-
return f'secret {name} labels mismatch in namespace {namespace}.'
227+
if annotations is not None and not is_subset(secret.metadata.annotations, annotations):
228+
return f'secret {name} annotations mismatch in namespace {namespace}.'
220229

230+
if labels is not None and not is_subset(secret.metadata.labels, labels):
231+
return f'secret {name} labels mismatch in namespace {namespace}.'
232+
221233
return None
222234

235+
# This is to wait before previous kubernetes operations are completed.
236+
sleep(self.before_validate_delay)
237+
223238
return self.retry(validate)
224239

225240
def retry(self, f: Callable[[], Optional[str]]) -> bool:

conformance/tests.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,48 @@ def test_patch_cluster_secret_match_namespaces(self):
162162
f'secret {name} should be in all user namespaces'
163163
)
164164

165+
def test_patch_cluster_secret_avoid_namespaces(self):
166+
name = "dynamic-cluster-secret-avoid-namespaces"
167+
username_data = "MTIzNDU2Cg=="
168+
169+
# Create a secret in all user namespaces
170+
self.cluster_secret_manager.create_cluster_secret(
171+
name=name,
172+
data={"username": username_data},
173+
match_namespace=USER_NAMESPACES
174+
)
175+
176+
# We expect the secret to be in all user namespaces
177+
self.assertTrue(
178+
self.cluster_secret_manager.validate_namespace_secrets(
179+
name=name,
180+
data={"username": username_data},
181+
namespaces=USER_NAMESPACES,
182+
)
183+
)
184+
185+
# Update the cluster avoid_namespaces to exclude second namespace
186+
self.cluster_secret_manager.update_data_cluster_secret(
187+
name=name,
188+
data={"username": username_data},
189+
match_namespace=USER_NAMESPACES,
190+
avoid_namespaces=[
191+
USER_NAMESPACES[1]
192+
],
193+
)
194+
195+
self.assertTrue(
196+
self.cluster_secret_manager.validate_namespace_secrets(
197+
name=name,
198+
data={"username": username_data},
199+
namespaces=[
200+
USER_NAMESPACES[1]
201+
],
202+
check_missing=True,
203+
),
204+
f'secret {name} should be deleted from second namespace'
205+
)
206+
165207
def test_simple_cluster_secret_deleted(self):
166208
name = "simple-cluster-secret-deleted"
167209
username_data = "MTIzNDU2Cg=="
@@ -188,7 +230,7 @@ def test_simple_cluster_secret_deleted(self):
188230
self.cluster_secret_manager.validate_namespace_secrets(
189231
name=name,
190232
data={"username": username_data},
191-
namespaces=[],
233+
check_missing=True,
192234
),
193235
f'secret {name} should be deleted from all namespaces.'
194236
)

src/handlers.py

+9-12
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,23 @@ def on_delete(
5050
logger.debug(f'csec {uid} deleted from memory ok')
5151

5252

53+
@kopf.on.field('clustersecret.io', 'v1', 'clustersecrets', field='avoidNamespaces')
5354
@kopf.on.field('clustersecret.io', 'v1', 'clustersecrets', field='matchNamespace')
54-
def on_field_match_namespace(
55+
def on_fields_avoid_or_match_namespace(
5556
old: Optional[List[str]],
5657
new: List[str],
5758
name: str,
5859
body,
5960
uid: str,
6061
logger: logging.Logger,
62+
reason: kopf.Reason,
6163
**_,
6264
):
63-
logger.debug(f'Namespaces changed: {old} -> {new}')
64-
65-
if old is None:
65+
if reason == "create":
6666
logger.debug('This is a new object: Ignoring.')
6767
return
6868

69+
logger.debug(f'Avoid or match namespaces changed: {old} -> {new}')
6970
logger.debug(f'Updating Object body == {body}')
7071

7172
syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', [])
@@ -80,7 +81,7 @@ def on_field_match_namespace(
8081
sync_secret(logger, secret_namespace, body, v1)
8182

8283
for secret_namespace in to_remove:
83-
delete_secret(logger, secret_namespace, name, v1=v1)
84+
delete_secret(logger, secret_namespace, name, v1)
8485

8586
cached_cluster_secret = csecs_cache.get_cluster_secret(uid)
8687
if cached_cluster_secret is None:
@@ -113,13 +114,14 @@ def on_field_data(
113114
name: str,
114115
uid: str,
115116
logger: logging.Logger,
117+
reason: kopf.Reason,
116118
**_,
117119
):
118-
logger.debug(f'Data changed: {old} -> {new}')
119-
if old is None:
120+
if reason == "create":
120121
logger.debug('This is a new object: Ignoring')
121122
return
122123

124+
logger.debug(f'Data changed: {old} -> {new}')
123125
logger.debug(f'Updating Object body == {body}')
124126
syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', [])
125127

@@ -197,11 +199,6 @@ async def create_fn(
197199
for ns in matchedns:
198200
sync_secret(logger, ns, body, v1)
199201

200-
# store status in memory
201-
cached_cluster_secret = csecs_cache.get_cluster_secret(uid)
202-
if cached_cluster_secret is None:
203-
logger.error('Received an event for an unknown ClusterSecret.')
204-
205202
# Updating the cache
206203
csecs_cache.set_cluster_secret(BaseClusterSecret(
207204
uid=uid,

src/tests/test_handlers.py

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def test_on_field_data_cache(self):
4343
name="mysecret",
4444
uid="mysecretuid",
4545
logger=self.logger,
46+
reason="update",
4647
)
4748

4849
# New data should be in the cache.
@@ -99,6 +100,7 @@ def test_on_field_data_sync(self):
99100
name="mysecret",
100101
uid="mysecretuid",
101102
logger=self.logger,
103+
reason="update",
102104
)
103105

104106
# Namespaced secret should be updated.
@@ -226,6 +228,7 @@ def read_namespace(name, **kwargs):
226228
name="mysecret",
227229
uid="mysecretuid",
228230
logger=self.logger,
231+
reason="update",
229232
)
230233

231234
# Namespaced secret should be updated with the new data.

0 commit comments

Comments
 (0)