Skip to content

Commit 6b70538

Browse files
authored
Merge branch 'master' into replaceExistingChartValuesExpose
2 parents d1ef40a + 4e1ee27 commit 6b70538

File tree

5 files changed

+126
-17
lines changed

5 files changed

+126
-17
lines changed

.github/workflows/py-unit-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Pytest
22

3-
on: [push]
3+
on: [push,pull_request]
44

55
jobs:
66
build:

charts/cluster-secret/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,5 @@ For older kubernes (<1.19) use the image tag "0.0.6" in yaml/02_deployment.yaml
6666

6767
```bash
6868
helm repo add clustersecret https://charts.clustersecret.com/
69-
helm install clustersecret clustersecret/cluster-secret --version 0.4.2 -n clustersecret --create-namespace
69+
helm install clustersecret clustersecret/cluster-secret --version 0.4.3 -n clustersecret --create-namespace
7070
```

src/consts.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99

1010
CLUSTER_SECRET_LABEL = "clustersecret.io"
1111

12-
BLACK_LISTED_ANNOTATIONS = ["kopf.zalando.org", "kubectl.kubernetes.io"]
12+
BLACK_LISTED_ANNOTATIONS = ["kopf.zalando.org", "kubectl.kubernetes.io"]
13+
BLACK_LISTED_LABELS = ["app.kubernetes.io"]

src/kubernetes_utils.py

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import logging
22
from datetime import datetime
3-
from typing import Optional, Dict, Any, List, Mapping
3+
from typing import Optional, Dict, Any, List, Mapping, Tuple, Iterator
44
import re
55

66
import kopf
77
from kubernetes.client import CoreV1Api, CustomObjectsApi, exceptions, V1ObjectMeta, rest, V1Secret
88

99
from os_utils import get_replace_existing, get_version
1010
from consts import CREATE_BY_ANNOTATION, LAST_SYNC_ANNOTATION, VERSION_ANNOTATION, BLACK_LISTED_ANNOTATIONS, \
11-
CREATE_BY_AUTHOR, CLUSTER_SECRET_LABEL
11+
BLACK_LISTED_LABELS, CREATE_BY_AUTHOR, CLUSTER_SECRET_LABEL
1212

1313

1414
def patch_clustersecret_status(
@@ -286,27 +286,36 @@ def create_secret_metadata(
286286
Kubernetes metadata object with ClusterSecret annotations.
287287
"""
288288

289-
_labels = {
289+
def filter_dict(
290+
prefixes: List[str],
291+
base: Dict[str, str],
292+
source: Optional[Mapping[str, str]] = None
293+
) -> Iterator[Tuple[str, str]]:
294+
""" Remove potential useless / dangerous annotations and labels"""
295+
for item in base.items():
296+
yield item
297+
if source is not None:
298+
for item in source.items():
299+
key, _ = item
300+
if not any(key.startswith(prefix) for prefix in prefixes):
301+
yield item
302+
303+
base_labels = {
290304
CLUSTER_SECRET_LABEL: 'true'
291305
}
292-
_labels.update(labels or {})
293-
294-
_annotations = {
306+
base_annotations = {
295307
CREATE_BY_ANNOTATION: CREATE_BY_AUTHOR,
296308
VERSION_ANNOTATION: get_version(),
297309
LAST_SYNC_ANNOTATION: datetime.now().isoformat(),
298310
}
299-
_annotations.update(annotations or {})
300-
301-
# Remove potential useless / dangerous annotations
302-
_annotations = {key: value for key, value in _annotations.items() if
303-
not any(key.startswith(prefix) for prefix in BLACK_LISTED_ANNOTATIONS)}
304311

312+
_annotations = filter_dict(BLACK_LISTED_ANNOTATIONS, base_annotations, annotations)
313+
_labels = filter_dict(BLACK_LISTED_LABELS, base_labels, labels)
305314
return V1ObjectMeta(
306315
name=name,
307316
namespace=namespace,
308-
annotations=_annotations,
309-
labels=_labels,
317+
annotations=dict(_annotations),
318+
labels=dict(_labels),
310319
)
311320

312321

src/tests/test_kubernetes_utils.py

+100-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1+
import datetime
12
import logging
23
import unittest
4+
from typing import Tuple, Callable, Union
35
from unittest.mock import Mock
46

57
from kubernetes.client import V1ObjectMeta
6-
from kubernetes_utils import get_ns_list
8+
9+
from consts import CREATE_BY_ANNOTATION, LAST_SYNC_ANNOTATION, VERSION_ANNOTATION, BLACK_LISTED_ANNOTATIONS, \
10+
BLACK_LISTED_LABELS, CREATE_BY_AUTHOR, CLUSTER_SECRET_LABEL
11+
from kubernetes_utils import get_ns_list, create_secret_metadata
12+
from os_utils import get_version
713

814
USER_NAMESPACE_COUNT = 10
915
initial_namespaces = ['default', 'kube-node-lease', 'kube-public', 'kube-system']
1016
user_namespaces = [f'example-{index}' for index in range(USER_NAMESPACE_COUNT)]
1117

1218

19+
def is_iso_format(date: str) -> bool:
20+
"""check whether a date string parses correctly as ISO 8601 format"""
21+
try:
22+
datetime.datetime.fromisoformat(date)
23+
return True
24+
except (TypeError, ValueError):
25+
return False
26+
27+
1328
class TestClusterSecret(unittest.TestCase):
1429

1530
def test_get_ns_list(self):
@@ -69,3 +84,87 @@ def test_get_ns_list(self):
6984
)),
7085
msg=case['name'],
7186
)
87+
88+
def test_create_secret_metadata(self) -> None:
89+
90+
expected_base_label_key = CLUSTER_SECRET_LABEL
91+
expected_base_label_value = 'true'
92+
93+
# key, value pairs, where the value can be a string or validation function
94+
expected_base_annotations: list[Tuple[str, Union[str, Callable[[str], bool]]]] = [
95+
(CREATE_BY_ANNOTATION, CREATE_BY_AUTHOR),
96+
(VERSION_ANNOTATION, get_version()),
97+
# Since LAST_SYNC_ANNOTATION is a date string which isn't easily validated by string equality
98+
# have the function 'is_iso_format' validate the value of this annotation.
99+
(LAST_SYNC_ANNOTATION, is_iso_format)
100+
]
101+
102+
attributes_black_lists = dict(
103+
labels=BLACK_LISTED_LABELS,
104+
annotations=BLACK_LISTED_ANNOTATIONS,
105+
)
106+
107+
test_cases: list[Tuple[dict[str, str], dict[str, str]]] = [
108+
# Annotations, Labels
109+
(
110+
{},
111+
{}
112+
),
113+
(
114+
{},
115+
{"modifiedAt": "1692462880",
116+
"name": "prometheus-operator",
117+
"owner": "helm",
118+
"status": "superseded",
119+
"version": "1"}
120+
),
121+
(
122+
{"managed-by": "argocd.argoproj.io"},
123+
{"argocd.argoproj.io/secret-type": "repository"}
124+
),
125+
(
126+
{"argocd.argoproj.io/compare-options": "ServerSideDiff=true",
127+
"argocd.argoproj.io/sync-wave": "4"},
128+
{"app.kubernetes.io/instance": "cluster-secret"}
129+
)
130+
]
131+
132+
for annotations, labels in test_cases:
133+
134+
subject: V1ObjectMeta = create_secret_metadata(
135+
name='test_secret',
136+
namespace='test_namespace',
137+
annotations=annotations,
138+
labels=labels
139+
)
140+
141+
self.assertIsInstance(obj=subject, cls=V1ObjectMeta, msg='returned value has correct type')
142+
143+
for attribute, black_list in attributes_black_lists.items():
144+
attribute_object = subject.__getattribute__(attribute)
145+
self.assertIsNotNone(obj=attribute_object, msg=f'attribute "{attribute}" is not None')
146+
147+
for key in attribute_object.keys():
148+
self.assertIsInstance(obj=key, cls=str, msg=f'the {attribute} key is a string')
149+
for black_listed_label_prefix in black_list:
150+
self.assertFalse(
151+
expr=key.startswith(black_listed_label_prefix),
152+
msg=f'{attribute} key does not match black listed prefix'
153+
)
154+
155+
# This tells mypy that those attributes are not None
156+
assert subject.labels is not None
157+
assert subject.annotations is not None
158+
159+
self.assertEqual(
160+
first=subject.labels[expected_base_label_key],
161+
second=expected_base_label_value,
162+
msg='expected base label is present'
163+
)
164+
for key, value_expectation in expected_base_annotations:
165+
validator = value_expectation if callable(value_expectation) else value_expectation.__eq__
166+
value = subject.annotations[key]
167+
self.assertTrue(
168+
expr=validator(value),
169+
msg=f'expected base annotation with key {key} is present and its value {value} is as expected'
170+
)

0 commit comments

Comments
 (0)