Skip to content

Commit

Permalink
Merge pull request #100 from axel7083/feature/labels&annotations
Browse files Browse the repository at this point in the history
feat: adding support for propagating annotations and labels
  • Loading branch information
zakkg3 authored Sep 25, 2023
2 parents 77602f4 + ba0212f commit bab429d
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 60 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.16.0-0'
type: application
version: 0.2.1
version: 0.2.2
icon: https://clustersecret.io/assets/csninjasmall.png
sources:
- https://github.com/zakkg3/ClusterSecret
Expand Down
1 change: 1 addition & 0 deletions charts/cluster-secret/templates/role-cluster-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ rules:
- list
- watch
- patch
- get
- apiGroups:
- ""
resources:
Expand Down
4 changes: 2 additions & 2 deletions charts/cluster-secret/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ clustersecret:
tag: 0.0.10
# use tag-alt for ARM and other alternative builds - read the readme for more information
# If Clustersecret is about to create a secret and then it founds it exists:
# Default is to ignore it. (to not loose any unintentional data)
# Default is to ignore it. (to not loose any unintentional data)
# It can also reeplace it. Just uncommenting next line.
#replace_existing: 'true'
# replace_existing: 'true'
kubernetesClusterDomain: cluster.local
70 changes: 56 additions & 14 deletions conformance/k8s_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import time
from typing import Dict, Optional, List, Callable, Any
from typing import Dict, Optional, List, Callable, Mapping, Any
from kubernetes import client, config
from kubernetes.client import V1Secret, CoreV1Api, CustomObjectsApi
from kubernetes.client.rest import ApiException
from time import sleep


def is_subset(_set: Optional[Mapping[str, str]], _subset: Optional[Mapping[str, str]]) -> bool:
if _set is None:
return _subset is None

for key, item in _subset.items():
if _set.get(key, None) != item:
return False
return True


def wait_for_pod_ready_with_events(pod_selector: dict, namespace: str, timeout_seconds: int = 300):
"""
Wait for a pod to be ready in the specified namespace and print all events.
Expand Down Expand Up @@ -52,6 +62,7 @@ class ClusterSecretManager:
def __init__(self, custom_objects_api: CustomObjectsApi, api_instance: CoreV1Api):
self.custom_objects_api: CustomObjectsApi = custom_objects_api
self.api_instance: CoreV1Api = api_instance
# immutable after
self.retry_attempts = 3
self.retry_delay = 5

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

def validate_namespace_secrets(
self, name: str,
self,
name: str,
data: Dict[str, str],
namespaces: Optional[List[str]] = None
namespaces: Optional[List[str]] = None,
labels: Optional[Dict[str, str]] = None,
annotations: Optional[Dict[str, str]] = None,
) -> bool:
"""
Parameters
----------
name
data
name: str
data: Dict[str, str]
namespaces: Optional[List[str]]
If None, it means the secret should be present in ALL namespaces
annotations: Optional[Dict[str, str]]
labels: Optional[Dict[str, str]]
Returns
-------
"""
all_namespaces = [item.metadata.name for item in self.api_instance.list_namespace().items]

def validate():
def validate() -> Optional[str]:
for namespace in all_namespaces:

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

if namespaces is not None and namespace not in namespaces:
if secret is None:
continue
return False
return f''

if secret is None:
return f'secret {name} is none in namespace {namespace}.'

if secret.data != data:
return f'secret {name} data 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 secret is None or secret.data != data:
return False
if labels is not None and not is_subset(secret.metadata.labels, labels):
return f'secret {name} labels mismatch in namespace {namespace}.'

return True
return None

return self.retry(validate)

def retry(self, f: Callable[[], bool]) -> bool:
while self.retry_attempts > 0:
if f():
def retry(self, f: Callable[[], Optional[str]]) -> bool:
"""
Utility function
Parameters
----------
f
Returns
-------
"""
retry: int = self.retry_attempts
err: Optional[str] = None

while retry > 0:
err = f()
if err is None:
return True
sleep(self.retry_delay)
self.retry_attempts -= 1
retry -= 1

if err is not None:
print(f"Retry attempts exhausted. Last error: {err}")
return False

def cleanup(self):
Expand Down
27 changes: 27 additions & 0 deletions conformance/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,33 @@ def test_value_from_with_keys_cluster_secret(self):
msg=f'Cluster secret should take the data from the {secret_name} secret but only the keys specified.'
)

def test_simple_cluster_secret_with_annotation(self):
name = "simple-cluster-secret-annotation"
username_data = "MTIzNDU2Cg=="
annotations = {
'custom-annotation': 'example',
}
cluster_secret_manager = ClusterSecretManager(
custom_objects_api=custom_objects_api,
api_instance=api_instance
)

cluster_secret_manager.create_cluster_secret(
name=name,
namespace=USER_NAMESPACES[0],
data={"username": username_data},
annotations=annotations,
)

# We expect the secret to be in ALL namespaces
self.assertTrue(
cluster_secret_manager.validate_namespace_secrets(
name=name,
data={"username": username_data},
annotations=annotations
)
)


if __name__ == '__main__':
unittest.main()
5 changes: 5 additions & 0 deletions src/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
"""

CREATE_BY_ANNOTATION = 'clustersecret.io/created-by'
CREATE_BY_AUTHOR = 'ClusterSecrets'
LAST_SYNC_ANNOTATION = 'clustersecret.io/last-sync'
VERSION_ANNOTATION = 'clustersecret.io/version'

CLUSTER_SECRET_LABEL = "clustersecret.io"

BLACK_LISTED_ANNOTATIONS = ["kopf.zalando.org", "kubectl.kubernetes.io"]
14 changes: 11 additions & 3 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def on_field_data(
old: Dict[str, str],
new: Dict[str, str],
body: Dict[str, Any],
meta: kopf.Meta,
name: str,
logger: logging.Logger,
**_,
Expand All @@ -122,17 +123,23 @@ def on_field_data(
logger.debug(f'Updating Object body == {body}')
syncedns = body.get('status', {}).get('create_fn', {}).get('syncedns', [])

secret_type = body.get('type', default='Opaque')
secret_type = body.get('type', 'Opaque')

for ns in syncedns:
logger.info(f'Re Syncing secret {name} in ns {ns}')
body = client.V1Secret(
api_version='v1',
data=new,
data={str(key): str(value) for key, value in new.items()},
kind='Secret',
metadata=create_secret_metadata(name=name, namespace=ns),
metadata=create_secret_metadata(
name=name,
namespace=ns,
annotations={str(key): str(value) for key, value in meta.annotations.items()},
labels={str(key): str(value) for key, value in meta.labels.items()},
),
type=secret_type,
)
logger.debug(f'body: {body}')
# Ensuring the secret still exist.
if secret_exists(logger=logger, name=name, namespace=ns, v1=v1):
response = v1.replace_namespaced_secret(name=name, namespace=ns, body=body)
Expand Down Expand Up @@ -237,5 +244,6 @@ async def startup_fn(logger: logging.Logger, **_):
name=metadata.get('name'),
namespace=metadata.get('namespace'),
data=item.get('data'),
synced_namespace=item.get('status', {}).get('create_fn', {}).get('syncedns', []),
)
)
Loading

0 comments on commit bab429d

Please sign in to comment.