Skip to content

Commit bab429d

Browse files
authored
Merge pull request #100 from axel7083/feature/labels&annotations
feat: adding support for propagating annotations and labels
2 parents 77602f4 + ba0212f commit bab429d

File tree

9 files changed

+175
-60
lines changed

9 files changed

+175
-60
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.16.0-0'
55
type: application
6-
version: 0.2.1
6+
version: 0.2.2
77
icon: https://clustersecret.io/assets/csninjasmall.png
88
sources:
99
- https://github.com/zakkg3/ClusterSecret

charts/cluster-secret/templates/role-cluster-rbac.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ rules:
4242
- list
4343
- watch
4444
- patch
45+
- get
4546
- apiGroups:
4647
- ""
4748
resources:

charts/cluster-secret/values.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ clustersecret:
55
tag: 0.0.10
66
# use tag-alt for ARM and other alternative builds - read the readme for more information
77
# If Clustersecret is about to create a secret and then it founds it exists:
8-
# Default is to ignore it. (to not loose any unintentional data)
8+
# Default is to ignore it. (to not loose any unintentional data)
99
# It can also reeplace it. Just uncommenting next line.
10-
#replace_existing: 'true'
10+
# replace_existing: 'true'
1111
kubernetesClusterDomain: cluster.local

conformance/k8s_utils.py

+56-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import time
2-
from typing import Dict, Optional, List, Callable, Any
2+
from typing import Dict, Optional, List, Callable, Mapping, Any
33
from kubernetes import client, config
44
from kubernetes.client import V1Secret, CoreV1Api, CustomObjectsApi
55
from kubernetes.client.rest import ApiException
66
from time import sleep
77

88

9+
def is_subset(_set: Optional[Mapping[str, str]], _subset: Optional[Mapping[str, str]]) -> bool:
10+
if _set is None:
11+
return _subset is None
12+
13+
for key, item in _subset.items():
14+
if _set.get(key, None) != item:
15+
return False
16+
return True
17+
18+
919
def wait_for_pod_ready_with_events(pod_selector: dict, namespace: str, timeout_seconds: int = 300):
1020
"""
1121
Wait for a pod to be ready in the specified namespace and print all events.
@@ -52,6 +62,7 @@ class ClusterSecretManager:
5262
def __init__(self, custom_objects_api: CustomObjectsApi, api_instance: CoreV1Api):
5363
self.custom_objects_api: CustomObjectsApi = custom_objects_api
5464
self.api_instance: CoreV1Api = api_instance
65+
# immutable after
5566
self.retry_attempts = 3
5667
self.retry_delay = 5
5768

@@ -167,48 +178,79 @@ def get_kubernetes_secret(self, name: str, namespace: str) -> Optional[V1Secret]
167178
raise e
168179

169180
def validate_namespace_secrets(
170-
self, name: str,
181+
self,
182+
name: str,
171183
data: Dict[str, str],
172-
namespaces: Optional[List[str]] = None
184+
namespaces: Optional[List[str]] = None,
185+
labels: Optional[Dict[str, str]] = None,
186+
annotations: Optional[Dict[str, str]] = None,
173187
) -> bool:
174188
"""
175189
176190
Parameters
177191
----------
178-
name
179-
data
192+
name: str
193+
data: Dict[str, str]
180194
namespaces: Optional[List[str]]
181195
If None, it means the secret should be present in ALL namespaces
196+
annotations: Optional[Dict[str, str]]
197+
labels: Optional[Dict[str, str]]
182198
183199
Returns
184200
-------
185201
186202
"""
187203
all_namespaces = [item.metadata.name for item in self.api_instance.list_namespace().items]
188204

189-
def validate():
205+
def validate() -> Optional[str]:
190206
for namespace in all_namespaces:
191207

192208
secret = self.get_kubernetes_secret(name=name, namespace=namespace)
193209

194210
if namespaces is not None and namespace not in namespaces:
195211
if secret is None:
196212
continue
197-
return False
213+
return f''
214+
215+
if secret is None:
216+
return f'secret {name} is none in namespace {namespace}.'
217+
218+
if secret.data != data:
219+
return f'secret {name} data mismatch in namespace {namespace}.'
220+
221+
if annotations is not None and not is_subset(secret.metadata.annotations, annotations):
222+
return f'secret {name} annotations mismatch in namespace {namespace}.'
198223

199-
if secret is None or secret.data != data:
200-
return False
224+
if labels is not None and not is_subset(secret.metadata.labels, labels):
225+
return f'secret {name} labels mismatch in namespace {namespace}.'
201226

202-
return True
227+
return None
203228

204229
return self.retry(validate)
205230

206-
def retry(self, f: Callable[[], bool]) -> bool:
207-
while self.retry_attempts > 0:
208-
if f():
231+
def retry(self, f: Callable[[], Optional[str]]) -> bool:
232+
"""
233+
Utility function
234+
Parameters
235+
----------
236+
f
237+
238+
Returns
239+
-------
240+
241+
"""
242+
retry: int = self.retry_attempts
243+
err: Optional[str] = None
244+
245+
while retry > 0:
246+
err = f()
247+
if err is None:
209248
return True
210249
sleep(self.retry_delay)
211-
self.retry_attempts -= 1
250+
retry -= 1
251+
252+
if err is not None:
253+
print(f"Retry attempts exhausted. Last error: {err}")
212254
return False
213255

214256
def cleanup(self):

conformance/tests.py

+27
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,33 @@ def test_value_from_with_keys_cluster_secret(self):
268268
msg=f'Cluster secret should take the data from the {secret_name} secret but only the keys specified.'
269269
)
270270

271+
def test_simple_cluster_secret_with_annotation(self):
272+
name = "simple-cluster-secret-annotation"
273+
username_data = "MTIzNDU2Cg=="
274+
annotations = {
275+
'custom-annotation': 'example',
276+
}
277+
cluster_secret_manager = ClusterSecretManager(
278+
custom_objects_api=custom_objects_api,
279+
api_instance=api_instance
280+
)
281+
282+
cluster_secret_manager.create_cluster_secret(
283+
name=name,
284+
namespace=USER_NAMESPACES[0],
285+
data={"username": username_data},
286+
annotations=annotations,
287+
)
288+
289+
# We expect the secret to be in ALL namespaces
290+
self.assertTrue(
291+
cluster_secret_manager.validate_namespace_secrets(
292+
name=name,
293+
data={"username": username_data},
294+
annotations=annotations
295+
)
296+
)
297+
271298

272299
if __name__ == '__main__':
273300
unittest.main()

src/consts.py

+5
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@
33
"""
44

55
CREATE_BY_ANNOTATION = 'clustersecret.io/created-by'
6+
CREATE_BY_AUTHOR = 'ClusterSecrets'
67
LAST_SYNC_ANNOTATION = 'clustersecret.io/last-sync'
78
VERSION_ANNOTATION = 'clustersecret.io/version'
9+
10+
CLUSTER_SECRET_LABEL = "clustersecret.io"
11+
12+
BLACK_LISTED_ANNOTATIONS = ["kopf.zalando.org", "kubectl.kubernetes.io"]

src/handlers.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def on_field_data(
110110
old: Dict[str, str],
111111
new: Dict[str, str],
112112
body: Dict[str, Any],
113+
meta: kopf.Meta,
113114
name: str,
114115
logger: logging.Logger,
115116
**_,
@@ -122,17 +123,23 @@ def on_field_data(
122123
logger.debug(f'Updating Object body == {body}')
123124
syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', [])
124125

125-
secret_type = body.get('type', default='Opaque')
126+
secret_type = body.get('type', 'Opaque')
126127

127128
for ns in syncedns:
128129
logger.info(f'Re Syncing secret {name} in ns {ns}')
129130
body = client.V1Secret(
130131
api_version='v1',
131-
data=new,
132+
data={str(key): str(value) for key, value in new.items()},
132133
kind='Secret',
133-
metadata=create_secret_metadata(name=name, namespace=ns),
134+
metadata=create_secret_metadata(
135+
name=name,
136+
namespace=ns,
137+
annotations={str(key): str(value) for key, value in meta.annotations.items()},
138+
labels={str(key): str(value) for key, value in meta.labels.items()},
139+
),
134140
type=secret_type,
135141
)
142+
logger.debug(f'body: {body}')
136143
# Ensuring the secret still exist.
137144
if secret_exists(logger=logger, name=name, namespace=ns, v1=v1):
138145
response = v1.replace_namespaced_secret(name=name, namespace=ns, body=body)
@@ -237,5 +244,6 @@ async def startup_fn(logger: logging.Logger, **_):
237244
name=metadata.get('name'),
238245
namespace=metadata.get('namespace'),
239246
data=item.get('data'),
247+
synced_namespace=item.get('status', {}).get('create_fn', {}).get('syncedns', []),
240248
)
241249
)

0 commit comments

Comments
 (0)