Skip to content
This repository has been archived by the owner on Sep 14, 2020. It is now read-only.

Commit

Permalink
Merge pull request #203 from nolar/implicit-owners
Browse files Browse the repository at this point in the history
Assume the current object as an implicit owner for hierarchies
  • Loading branch information
nolar committed Oct 8, 2019
2 parents 55327f6 + 4941932 commit 267e23b
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 39 deletions.
13 changes: 9 additions & 4 deletions docs/hierarchies.rst
Expand Up @@ -39,8 +39,8 @@ Owner references
Kubernetes natively supports the owner references, when one object (child)
can be marked as "owned" by one or more other objects (owners or parents).

if the owner is deleted, its children will be deleted too, automatically,
and not additional handlers are needed.
If the owner is deleted, its children will be deleted too, automatically,
and no additional handlers are needed.

To mark an object or objects as owned by another object::

Expand All @@ -50,6 +50,11 @@ To unmark::

kopf.remove_owner_reference(objs, owner=owner)

The currently handled object is the owner by default (if not specified)::

kopf.append_owner_reference(objs)
kopf.remove_owner_reference(objs)

.. seealso::
:doc:`walkthrough/deletion`.

Expand Down Expand Up @@ -79,5 +84,5 @@ Adopting

All of the above can be done in one call::

kopf.adopt(obj, owner=owner)
kopf.adopt([obj1, obj2], owner=owner)
kopf.adopt(obj)
kopf.adopt([obj1, obj2])
4 changes: 2 additions & 2 deletions docs/walkthrough/deletion.rst
Expand Up @@ -40,7 +40,7 @@ Let's extend the creation handler:
import yaml
@kopf.on.create('zalando.org', 'v1', 'ephemeralvolumeclaims')
def create_fn(meta, body, spec, namespace, logger, **kwargs):
def create_fn(meta, spec, namespace, logger, **kwargs):
name = meta.get('name')
size = spec.get('size')
Expand All @@ -52,7 +52,7 @@ Let's extend the creation handler:
text = tmpl.format(name=name, size=size)
data = yaml.safe_load(text)
kopf.adopt(data, owner=body)
kopf.adopt(data)
api = kubernetes.client.CoreV1Api()
obj = api.create_namespaced_persistent_volume_claim(
Expand Down
2 changes: 1 addition & 1 deletion docs/walkthrough/updates.rst
Expand Up @@ -43,7 +43,7 @@ with one additional line:
text = tmpl.format(size=size, name=name)
data = yaml.safe_load(text)
kopf.adopt(data, owner=body)
kopf.adopt(data)
api = kubernetes.client.CoreV1Api()
obj = api.create_namespaced_persistent_volume_claim(
Expand Down
4 changes: 2 additions & 2 deletions examples/02-children/example.py
Expand Up @@ -4,7 +4,7 @@


@kopf.on.create('zalando.org', 'v1', 'kopfexamples')
def create_fn(body, spec, **kwargs):
def create_fn(spec, **kwargs):

# Render the pod yaml with some spec fields used in the template.
doc = yaml.safe_load(f"""
Expand All @@ -25,7 +25,7 @@ def create_fn(body, spec, **kwargs):
""")

# Make it our child: assign the namespace, name, labels, owner references, etc.
kopf.adopt(doc, owner=body)
kopf.adopt(doc)

# Actually create an object by requesting the Kubernetes API.
api = kubernetes.client.CoreV1Api()
Expand Down
4 changes: 2 additions & 2 deletions examples/99-all-at-once/example.py
Expand Up @@ -68,7 +68,7 @@ def wait_for_something():


@kopf.on.create('zalando.org', 'v1', 'kopfexamples')
def create_pod(body, **kwargs):
def create_pod(**kwargs):

# Render the pod yaml with some spec fields used in the template.
pod_data = yaml.safe_load(f"""
Expand All @@ -82,7 +82,7 @@ def create_pod(body, **kwargs):
""")

# Make it our child: assign the namespace, name, labels, owner references, etc.
kopf.adopt(pod_data, owner=body)
kopf.adopt(pod_data)
kopf.label(pod_data, {'application': 'kopf-example-10'})

# Actually create an object by requesting the Kubernetes API.
Expand Down
26 changes: 9 additions & 17 deletions kopf/reactor/handling.py
Expand Up @@ -500,15 +500,15 @@ async def _call_handler(
# Store the context of the current resource-object-event-handler, to be used in `@kopf.on.this`,
# and maybe other places, and consumed in the recursive `execute()` calls for the children.
# This replaces the multiple kwargs passing through the whole call stack (easy to forget).
sublifecycle_token = sublifecycle_var.set(lifecycle)
subregistry_token = subregistry_var.set(registries.SimpleRegistry(prefix=handler.id))
subexecuted_token = subexecuted_var.set(False)
handler_token = handler_var.set(handler)
cause_token = cause_var.set(cause)

# And call it. If the sub-handlers are not called explicitly, run them implicitly
# as if it was done inside of the handler (i.e. under try-finally block).
try:
with invocation.context([
(sublifecycle_var, lifecycle),
(subregistry_var, registries.SimpleRegistry(prefix=handler.id)),
(subexecuted_var, False),
(handler_var, handler),
(cause_var, cause),
]):
# And call it. If the sub-handlers are not called explicitly, run them implicitly
# as if it was done inside of the handler (i.e. under try-finally block).
result = await invocation.invoke(
handler.fn,
*args,
Expand All @@ -520,11 +520,3 @@ async def _call_handler(
await execute()

return result

finally:
# Reset the context to the parent's context, or to nothing (if already in a root handler).
sublifecycle_var.reset(sublifecycle_token)
subregistry_var.reset(subregistry_token)
subexecuted_var.reset(subexecuted_token)
handler_var.reset(handler_token)
cause_var.reset(cause_token)
22 changes: 20 additions & 2 deletions kopf/reactor/invocation.py
Expand Up @@ -6,20 +6,38 @@
All of this goes via the same invocation logic and protocol.
"""
import asyncio
import contextlib
import contextvars
import functools
from typing import Optional, Any, Union
from typing import Optional, Any, Union, List, Iterable, Iterator, Tuple

from kopf import config
from kopf.reactor import causation
from kopf.reactor import lifecycles
from kopf.reactor import registries
from kopf.structs import bodies
from kopf.structs import dicts

Invokable = Union[lifecycles.LifeCycleFn, registries.HandlerFn]


@contextlib.contextmanager
def context(
values: Iterable[Tuple[contextvars.ContextVar[Any], Any]],
) -> Iterator[None]:
"""
A context manager to set the context variables temporarily.
"""
tokens: List[Tuple[contextvars.ContextVar[Any], contextvars.Token[Any]]] = []
try:
for var, val in values:
token = var.set(val)
tokens.append((var, token))
yield
finally:
for var, token in reversed(tokens):
var.reset(token)


async def invoke(
fn: Invokable,
*args: Any,
Expand Down
53 changes: 44 additions & 9 deletions kopf/toolkits/hierarchies.py
Expand Up @@ -3,6 +3,7 @@
"""
from typing import Optional, Iterable, Iterator, cast, MutableMapping, Any, Union

from kopf.reactor import handling
from kopf.structs import bodies
from kopf.structs import dicts

Expand All @@ -12,15 +13,16 @@

def append_owner_reference(
objs: K8sObjects,
owner: bodies.Body,
owner: Optional[bodies.Body] = None,
) -> None:
"""
Append an owner reference to the resource(s), if it is not yet there.
Note: the owned objects are usually not the one being processed,
so the whole body can be modified, no patches are needed.
"""
owner_ref = bodies.build_owner_reference(owner)
real_owner = _guess_owner(owner)
owner_ref = bodies.build_owner_reference(real_owner)
for obj in cast(Iterator[K8sObject], dicts.walk(objs)):
refs = obj.setdefault('metadata', {}).setdefault('ownerReferences', [])
matching = [ref for ref in refs if ref.get('uid') == owner_ref['uid']]
Expand All @@ -30,15 +32,16 @@ def append_owner_reference(

def remove_owner_reference(
objs: K8sObjects,
owner: bodies.Body,
owner: Optional[bodies.Body] = None,
) -> None:
"""
Remove an owner reference to the resource(s), if it is there.
Note: the owned objects are usually not the one being processed,
so the whole body can be modified, no patches are needed.
"""
owner_ref = bodies.build_owner_reference(owner)
real_owner = _guess_owner(owner)
owner_ref = bodies.build_owner_reference(real_owner)
for obj in cast(Iterator[K8sObject], dicts.walk(objs)):
refs = obj.setdefault('metadata', {}).setdefault('ownerReferences', [])
matching = [ref for ref in refs if ref.get('uid') == owner_ref['uid']]
Expand Down Expand Up @@ -84,6 +87,13 @@ def harmonize_naming(
If the objects already have their own names, auto-naming is not applied,
and the existing names are used as is.
"""

# Try to use the current object being handled if possible.
if name is None:
real_owner = _guess_owner(None)
name = real_owner.get('metadata', {}).get('name', None)

# Set name/prefix based on the explicitly specified or guessed name.
for obj in cast(Iterator[K8sObject], dicts.walk(objs)):
if obj.get('metadata', {}).get('name', None) is None:
if strict:
Expand All @@ -104,20 +114,45 @@ def adjust_namespace(
It is a common practice to keep the children objects in the same
namespace as their owner, unless explicitly overridden at time of creation.
"""

# Try to use the current object being handled if possible.
if namespace is None:
real_owner = _guess_owner(None)
namespace = real_owner.get('metadata', {}).get('namespace', None)

# Set namespace based on the explicitly specified or guessed namespace.
for obj in cast(Iterator[K8sObject], dicts.walk(objs)):
obj.setdefault('metadata', {}).setdefault('namespace', namespace)


def adopt(
objs: K8sObjects,
owner: bodies.Body,
owner: Optional[bodies.Body] = None,
*,
nested: Optional[Iterable[dicts.FieldSpec]] = None,
) -> None:
"""
The children should be in the same namespace, named after their parent, and owned by it.
"""
append_owner_reference(objs, owner=owner)
harmonize_naming(objs, name=owner.get('metadata', {}).get('name', None))
adjust_namespace(objs, namespace=owner.get('metadata', {}).get('namespace', None))
label(objs, labels=owner.get('metadata', {}).get('labels', {}), nested=nested)
real_owner = _guess_owner(owner)
append_owner_reference(objs, owner=real_owner)
harmonize_naming(objs, name=real_owner.get('metadata', {}).get('name', None))
adjust_namespace(objs, namespace=real_owner.get('metadata', {}).get('namespace', None))
label(objs, labels=real_owner.get('metadata', {}).get('labels', {}), nested=nested)


def _guess_owner(
owner: Optional[bodies.Body],
) -> bodies.Body:
if owner is not None:
return owner

try:
cause = handling.cause_var.get()
except LookupError:
pass
else:
if cause is not None:
return cause.body

raise LookupError("Owner must be set explicitly, since running outside of a handler.")
111 changes: 111 additions & 0 deletions tests/hierarchies/test_contextual_owner.py
@@ -0,0 +1,111 @@
import logging

import pytest

import kopf
from kopf.reactor.causation import Reason, StateChangingCause, EventWatchingCause
from kopf.reactor.handling import cause_var
from kopf.reactor.invocation import context
from kopf.structs.bodies import Body, Meta, Labels, Event
from kopf.structs.patches import Patch

OWNER_API_VERSION = 'owner-api-version'
OWNER_NAMESPACE = 'owner-namespace'
OWNER_KIND = 'OwnerKind'
OWNER_NAME = 'owner-name'
OWNER_UID = 'owner-uid'
OWNER_LABELS: Labels = {'label-1': 'value-1', 'label-2': 'value-2'}
OWNER = Body(
apiVersion=OWNER_API_VERSION,
kind=OWNER_KIND,
metadata=Meta(
namespace=OWNER_NAMESPACE,
name=OWNER_NAME,
uid=OWNER_UID,
labels=OWNER_LABELS,
),
)


@pytest.fixture(params=['state-changing-cause', 'event-watching-cause'])
def owner(request, resource):
if request.param == 'state-changing-cause':
cause = StateChangingCause(
logger=logging.getLogger('kopf.test.fake.logger'),
resource=resource,
patch=Patch(),
body=OWNER,
initial=False,
reason=Reason.NOOP,
)
with context([(cause_var, cause)]):
yield
elif request.param == 'event-watching-cause':
cause = EventWatchingCause(
logger=logging.getLogger('kopf.test.fake.logger'),
resource=resource,
patch=Patch(),
body=OWNER,
type='irrelevant',
raw=Event(type='irrelevant', object=OWNER),
)
with context([(cause_var, cause)]):
yield
else:
raise RuntimeError(f"Wrong param for `owner` fixture: {request.param!r}")


def test_when_unset_for_owner_references_appending():
with pytest.raises(LookupError) as e:
kopf.append_owner_reference([])
assert 'Owner must be set explicitly' in str(e.value)


def test_when_unset_for_owner_references_removal():
with pytest.raises(LookupError) as e:
kopf.remove_owner_reference([])
assert 'Owner must be set explicitly' in str(e.value)


def test_when_unset_for_name_harmonization():
with pytest.raises(LookupError) as e:
kopf.harmonize_naming([])
assert 'Owner must be set explicitly' in str(e.value)


def test_when_unset_for_namespace_adjustment():
with pytest.raises(LookupError) as e:
kopf.adjust_namespace([])
assert 'Owner must be set explicitly' in str(e.value)


def test_when_unset_for_adopting():
with pytest.raises(LookupError) as e:
kopf.adopt([])
assert 'Owner must be set explicitly' in str(e.value)


def test_when_set_for_name_harmonization(owner):
obj = {}
kopf.harmonize_naming(obj)
assert obj['metadata']['generateName'].startswith(OWNER_NAME)


def test_when_set_for_namespace_adjustment(owner):
obj = {}
kopf.adjust_namespace(obj)
assert obj['metadata']['namespace'] == OWNER_NAMESPACE


def test_when_set_for_owner_references_appending(owner):
obj = {}
kopf.append_owner_reference(obj)
assert obj['metadata']['ownerReferences']
assert obj['metadata']['ownerReferences'][0]['uid'] == OWNER_UID


def test_when_set_for_owner_references_removal(owner):
obj = {}
kopf.append_owner_reference(obj) # assumed to work, tested above
kopf.remove_owner_reference(obj) # this one is being tested here
assert not obj['metadata']['ownerReferences']

0 comments on commit 267e23b

Please sign in to comment.