Skip to content

Commit

Permalink
Merge pull request #67 from HonakerM/add_resource_verification
Browse files Browse the repository at this point in the history
Add Individual Resource Verification Override
  • Loading branch information
gabe-l-hart committed May 2, 2024
2 parents f38d89a + 18e4838 commit d651383
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 24 deletions.
17 changes: 9 additions & 8 deletions oper8/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand All @@ -251,15 +252,15 @@ 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

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
26 changes: 20 additions & 6 deletions oper8/dag/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)
4 changes: 3 additions & 1 deletion oper8/managed_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions oper8/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion oper8/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
15 changes: 11 additions & 4 deletions oper8/verify_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ##############################################################


Expand All @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_verify_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##
##########
Expand Down

0 comments on commit d651383

Please sign in to comment.