diff --git a/oper8/component.py b/oper8/component.py index 1180987..ad23108 100644 --- a/oper8/component.py +++ b/oper8/component.py @@ -21,9 +21,9 @@ from .exceptions import assert_cluster from .managed_object import ManagedObject from .patch import apply_patches -from .session import VERIFY_FUNCTION, Session +from .session import COMPONENT_VERIFY_FUNCTION, Session from .utils import abstractclassproperty, sanitize_for_serialization -from .verify_resources import verify_resource +from .verify_resources import RESOURCE_VERIFY_FUNCTION, verify_resource log = alog.use_channel("COMP-BASE") @@ -232,6 +232,7 @@ def add_resource( self, name: str, # pylint: disable=redefined-builtin obj: Any, + verify_function: Optional[RESOURCE_VERIFY_FUNCTION] = None, ) -> Optional[ ResourceNode ]: # pylint: disable=unused-argument, redefined-builtin, invalid-name @@ -251,7 +252,7 @@ def add_resource( # Add namespace to obj if not present obj.setdefault("metadata", {}).setdefault("namespace", self.session_namespace) - node = ResourceNode(name, obj) + node = ResourceNode(name, obj, verify_function) self.graph.add_node(node) return node @@ -259,7 +260,7 @@ def add_dependency( self, session: Session, *components: "Component", - verify_function: Optional[VERIFY_FUNCTION] = None, + verify_function: Optional[COMPONENT_VERIFY_FUNCTION] = None, ): """This add_dependency function sets up a dependency between this component and a list of other components. To add a dependency between resources inside @@ -385,6 +386,7 @@ def _default_verify(self, session, is_subsystem=False): session=session, is_subsystem=is_subsystem, namespace=resource.namespace, + verify_function=resource.verify_function, ): log.debug("[%s/%s] not verified", resource.kind, resource.name) return False @@ -468,7 +470,7 @@ def __render(self, session): # Iterate all ApiObject children in dependency order and perform the # rendering, including patches and backend modifications. self._managed_objects = [] - for name, obj in resource_list: + for name, obj, verify_func in resource_list: # Apply any patches to this object log.debug2("Applying patches to managed object: %s", name) log.debug4("Before Patching: %s", obj) @@ -495,7 +497,7 @@ def __render(self, session): obj = self.update_object_definition(session, name, obj) # Add the resource to the set managed by the is component - managed_obj = ManagedObject(obj) + managed_obj = ManagedObject(obj, verify_func) log.debug2("Adding managed object: %s", managed_obj) log.debug4("Final Definition: %s", obj) self._managed_objects.append(managed_obj) @@ -521,7 +523,6 @@ def __gather_resources(self, session) -> List[Tuple[str, dict]]: for child in children: # Construct the managed object with its internal name child_name = ".".join([self.name, child.get_name()]) - child_obj = child.get_data() - resource_list.append((child_name, child_obj)) + resource_list.append((child_name, child.manifest, child.verify_function)) return resource_list diff --git a/oper8/dag/node.py b/oper8/dag/node.py index 29e02ac..cff690c 100644 --- a/oper8/dag/node.py +++ b/oper8/dag/node.py @@ -2,7 +2,7 @@ This module contains a collection of classes for implementing nodes of a Graph """ # Standard -from typing import Any, List, Optional +from typing import Any, Callable, List, Optional class Node: @@ -114,11 +114,15 @@ def __hash__(self) -> str: class ResourceNode(Node): - """Class for representing a kubernetes resource in the Graph""" + """Class for representing a kubernetes resource in the Graph with + a function for verifying said resource""" - def __init__(self, name: str, manifest: dict): + def __init__( + self, name: str, manifest: dict, verify_func: Optional[Callable] = None + ): # Override init to require name/manifest parameters super().__init__(name, manifest) + self._verify_function = verify_func ## ApiObject Parameters and Functions ###################################### @property @@ -129,23 +133,33 @@ def api_group(self) -> str: @property def api_version(self) -> str: """The full kubernetes apiVersion""" - return self.get_data().get("apiVersion") + return self.manifest.get("apiVersion") @property def kind(self) -> str: """The resource kind""" - return self.get_data().get("kind") + return self.manifest.get("kind") @property def metadata(self) -> dict: """The full resource metadata dict""" - return self.get_data().get("metadata", {}) + return self.manifest.get("metadata", {}) @property def name(self) -> str: """The resource metadata.name""" return self.metadata.get("name") + @property + def manifest(self) -> dict: + """The resource manifest""" + return self.get_data() + + @property + def verify_function(self) -> Optional[Callable]: + """The resource manifest""" + return self._verify_function + def add_dependency(self, node: "ResourceNode"): """Add a child dependency to this node""" self.add_child(node) diff --git a/oper8/managed_object.py b/oper8/managed_object.py index c3e5fde..27f13a4 100644 --- a/oper8/managed_object.py +++ b/oper8/managed_object.py @@ -2,6 +2,7 @@ Helper object to represent a kubernetes object that is managed by the operator """ # Standard +from typing import Callable, Optional import uuid KUBE_LIST_IDENTIFIER = "List" @@ -10,7 +11,7 @@ class ManagedObject: # pylint: disable=too-many-instance-attributes """Basic struct to represent a managed kubernetes object""" - def __init__(self, definition): + def __init__(self, definition: dict, verify_function: Optional[Callable] = None): self.kind = definition.get("kind") self.metadata = definition.get("metadata", {}) self.name = self.metadata.get("name") @@ -19,6 +20,7 @@ def __init__(self, definition): self.resource_version = self.metadata.get("resourceVersion") self.api_version = definition.get("apiVersion") self.definition = definition + self.verify_function = verify_function # If resource is not list then check name if KUBE_LIST_IDENTIFIER not in self.kind: diff --git a/oper8/session.py b/oper8/session.py index 0dd759c..104492d 100644 --- a/oper8/session.py +++ b/oper8/session.py @@ -26,10 +26,10 @@ # Maximum length for a kubernetes name MAX_NAME_LEN = 63 -# Type definition for the signature of a verify function +# Type definition for the signature of a component verify function # NOTE: I'm not sure why pylint dislikes this name. In my view, this is a shared # global which should have all-caps casing. -VERIFY_FUNCTION = Callable[["Session"], bool] # pylint: disable=invalid-name +COMPONENT_VERIFY_FUNCTION = Callable[["Session"], bool] # pylint: disable=invalid-name # Helper Definition to define when a session should use its own namespace # or the one passed in as an argument @@ -232,7 +232,7 @@ def add_component_dependency( self, component: Union[str, COMPONENT_INSTANCE_TYPE], upstream_component: Union[str, COMPONENT_INSTANCE_TYPE], - verify_function: Optional[VERIFY_FUNCTION] = None, + verify_function: Optional[COMPONENT_VERIFY_FUNCTION] = None, ): """Add a dependency indicating that one component requires an upstream component to be deployed before it can be deployed. @@ -338,7 +338,7 @@ def get_components(self, disabled: bool = False) -> List[COMPONENT_INSTANCE_TYPE def get_component_dependencies( self, component: Union[str, COMPONENT_INSTANCE_TYPE], - ) -> List[Tuple[COMPONENT_INSTANCE_TYPE, Optional[VERIFY_FUNCTION]]]: + ) -> List[Tuple[COMPONENT_INSTANCE_TYPE, Optional[COMPONENT_VERIFY_FUNCTION]]]: """Get the list of (upstream_name, verify_function) tuples for a given component. diff --git a/oper8/utils.py b/oper8/utils.py index 2ed9dfe..5cc265e 100644 --- a/oper8/utils.py +++ b/oper8/utils.py @@ -139,7 +139,7 @@ def sanitize_for_serialization(obj): # pylint: disable=too-many-return-statemen elif isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() elif isinstance(obj, ResourceNode): - return sanitize_for_serialization(obj.get_data()) + return sanitize_for_serialization(obj.manifest) elif isinstance(obj, property): return sanitize_for_serialization(obj.fget()) diff --git a/oper8/verify_resources.py b/oper8/verify_resources.py index 38c211a..f50ac2f 100644 --- a/oper8/verify_resources.py +++ b/oper8/verify_resources.py @@ -6,7 +6,7 @@ # Standard from datetime import datetime from functools import partial -from typing import List, Optional +from typing import Callable, List, Optional # Third Party import dateutil.parser @@ -27,6 +27,12 @@ PROGRESSING_CONDITION_KEY = "Progressing" NEW_RS_AVAILABLE_REASON = "NewReplicaSetAvailable" +# Type definition for the signature of a resource verify function +# NOTE: I'm not sure why pylint dislikes this name. In my view, this is a shared +# global which should have all-caps casing. +RESOURCE_VERIFY_FUNCTION = Callable[[dict], bool] # pylint: disable=invalid-name + + ## Main Functions ############################################################## @@ -39,9 +45,10 @@ def verify_resource( # Use a predfined _SESSION_NAMESPACE default instead of None to differentiate between # nonnamespaced resources (which pass None) and those that use session.namespace namespace: Optional[str] = _SESSION_NAMESPACE, + verify_function: Optional[RESOURCE_VERIFY_FUNCTION] = None, is_subsystem: bool = False, - condition_type: str = None, - timestamp_key: str = None, + condition_type: Optional[str] = None, + timestamp_key: Optional[str] = None, ) -> bool: """Verify a resource detailed in a ManagedObject. @@ -107,7 +114,7 @@ def verify_resource( ) # Run the appropriate verification function if there is one available - verify_fn = _resource_verifiers.get(kind) + verify_fn = verify_function or _resource_verifiers.get(kind) if not verify_fn and is_subsystem: log.debug("Using oper8 subsystem verifier for [%s/%s]", kind, name) verify_fn = partial( diff --git a/tests/test_component.py b/tests/test_component.py index 6296872..31add6d 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -206,6 +206,36 @@ def build_chart(self, session): assert objs[0].metadata.namespace == "unique" +def test_custom_resource_verify(): + """Make sure that a user can override the verify function for a particular resource.""" + session = setup_session() + bar = {"kind": "Foo", "apiVersion": "v1", "metadata": {"name": "bar"}} + + class TestSuccessVerify(Component): + name = "bar" + + def build_chart(self, session): + self.add_resource("bar", bar) + + class TestFailedVerify(Component): + name = "bar_fail" + + def build_chart(self, session): + self.add_resource("bar", bar, lambda resource: False) + + suc_comp = TestSuccessVerify(session=session) + failed_comp = TestFailedVerify(session=session) + + with library_config(internal_name_annotation=True): + suc_comp.render_chart(session) + suc_comp.deploy(session) + assert suc_comp.verify(session) + + failed_comp.render_chart(session) + failed_comp.deploy(session) + assert not failed_comp.verify(session) + + def test_internal_name_annotation(): """Make sure the internal name annotation is added to output resources if configured diff --git a/tests/test_verify_resources.py b/tests/test_verify_resources.py index 6c1afca..8249b00 100644 --- a/tests/test_verify_resources.py +++ b/tests/test_verify_resources.py @@ -166,6 +166,15 @@ def test_verify_pod_null_namespace(): ) +def test_verify_pod_custom_verification(): + """Make sure a ready pod fails to verify with a custom override""" + assert not run_test_verify( + kind="Pod", + conditions=[make_condition("Ready", True)], + verify_function=lambda resource: False, + ) + + ########## ## Jobs ## ##########