Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding support for propagating annotations and labels #100

Merged
merged 8 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading