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 #344 from nolar/configurable-finalizer
Browse files Browse the repository at this point in the history
Make finalizer's name configurable
  • Loading branch information
nolar authored Apr 7, 2020
2 parents 03c6da7 + 574aaa0 commit fbc050d
Show file tree
Hide file tree
Showing 15 changed files with 115 additions and 73 deletions.
27 changes: 26 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,39 @@ or disconnects. The default is 0.1 seconds (nearly instant, but not flooding).

.. code-block:: python
import concurrent.futures
import kopf
@kopf.on.startup()
def configure(settings: kopf.OperatorSettings, **_):
settings.watching.server_timeout = 10 * 60
Finalizers
==========

A resource is blocked from deletion if the framework believes it is safer
to do so, e.g. if non-optional deletion handlers are present
or if daemons/timers are running at the moment.

For this, a finalizer_ is added to the object. It is removed when the framework
believes it is safe to release the object for actual deletion.

.. _finalizer: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers

The name of the finalizer can be configured:

.. code-block:: python
import kopf
@kopf.on.startup()
def configure(settings: kopf.OperatorSettings, **_):
settings.persistence.finalizer = 'my-operator.example.com/kopf-finalizer'
The default is the one that was hard-coded before:
``kopf.zalando.org/KopfFinalizerMarker``.


.. _progress-storing:

Handling progress
Expand Down
7 changes: 5 additions & 2 deletions kopf/reactor/causation.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def detect_resource_spawning_cause(

def detect_resource_changing_cause(
*,
finalizer: str,
raw_event: bodies.RawEvent,
body: bodies.Body,
old: Optional[bodies.BodyEssence] = None,
Expand Down Expand Up @@ -173,10 +174,12 @@ def detect_resource_changing_cause(
return ResourceChangingCause(reason=handlers.Reason.GONE, **kwargs)

# The finalizer has been just removed. We are fully done.
if finalizers.is_deletion_ongoing(body) and not finalizers.is_deletion_blocked(body):
deletion_is_ongoing = finalizers.is_deletion_ongoing(body=body)
deletion_is_blocked = finalizers.is_deletion_blocked(body=body, finalizer=finalizer)
if deletion_is_ongoing and not deletion_is_blocked:
return ResourceChangingCause(reason=handlers.Reason.FREE, **kwargs)

if finalizers.is_deletion_ongoing(body):
if deletion_is_ongoing:
return ResourceChangingCause(reason=handlers.Reason.DELETE, **kwargs)

# For an object seen for the first time (i.e. just-created), call the creation handlers,
Expand Down
10 changes: 6 additions & 4 deletions kopf/reactor/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async def process_resource_event(
All the internally provoked changes are intercepted, do not create causes,
and therefore do not call the handling logic.
"""
finalizer = settings.persistence.finalizer

# Recall what is stored about that object. Share it in little portions with the consumers.
# And immediately forget it if the object is deleted from the cluster (but keep in memory).
Expand Down Expand Up @@ -110,6 +111,7 @@ async def process_resource_event(
) if registry.resource_spawning_handlers[resource] else None

resource_changing_cause = causation.detect_resource_changing_cause(
finalizer=finalizer,
raw_event=raw_event,
resource=resource,
logger=logger,
Expand All @@ -127,7 +129,7 @@ async def process_resource_event(
# The high-level handlers are prevented if this event cycle is dedicated to the finalizer.
# The low-level handlers (on-event spying & daemon spawning) are still executed asap.
deletion_is_ongoing = finalizers.is_deletion_ongoing(body=body)
deletion_is_blocked = finalizers.is_deletion_blocked(body=body)
deletion_is_blocked = finalizers.is_deletion_blocked(body=body, finalizer=finalizer)
deletion_must_be_blocked = (
(resource_spawning_cause is not None and
registry.resource_spawning_handlers[resource].requires_finalizer(
Expand All @@ -142,12 +144,12 @@ async def process_resource_event(

if deletion_must_be_blocked and not deletion_is_blocked and not deletion_is_ongoing:
logger.debug("Adding the finalizer, thus preventing the actual deletion.")
finalizers.block_deletion(body=body, patch=patch)
finalizers.block_deletion(body=body, patch=patch, finalizer=finalizer)
resource_changing_cause = None # prevent further high-level processing this time

if not deletion_must_be_blocked and deletion_is_blocked:
logger.debug("Removing the finalizer, as there are no handlers requiring it.")
finalizers.allow_deletion(body=body, patch=patch)
finalizers.allow_deletion(body=body, patch=patch, finalizer=finalizer)
resource_changing_cause = None # prevent further high-level processing this time

# Invoke all the handlers that should or could be invoked at this processing cycle.
Expand Down Expand Up @@ -186,7 +188,7 @@ async def process_resource_event(
and not resource_spawning_delays \
and not resource_changing_delays:
logger.debug("Removing the finalizer, thus allowing the actual deletion.")
finalizers.allow_deletion(body=body, patch=patch)
finalizers.allow_deletion(body=body, patch=patch, finalizer=finalizer)

# Whatever was done, apply the accumulated changes to the object, or sleep-n-touch for delays.
# But only once, to reduce the number of API calls and the generated irrelevant events.
Expand Down
18 changes: 9 additions & 9 deletions kopf/storage/finalizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
from kopf.structs import bodies
from kopf.structs import patches

# A string marker to be put on the list of the finalizers to block
# the object from being deleted without the permission of the framework.
FINALIZER = 'kopf.zalando.org/KopfFinalizerMarker'
LEGACY_FINALIZER = 'KopfFinalizerMarker'


Expand All @@ -22,31 +19,34 @@ def is_deletion_ongoing(

def is_deletion_blocked(
body: bodies.Body,
finalizer: str,
) -> bool:
finalizers = body.get('metadata', {}).get('finalizers', [])
return FINALIZER in finalizers or LEGACY_FINALIZER in finalizers
return finalizer in finalizers or LEGACY_FINALIZER in finalizers


def block_deletion(
*,
body: bodies.Body,
patch: patches.Patch,
finalizer: str,
) -> None:
if not is_deletion_blocked(body=body):
if not is_deletion_blocked(body=body, finalizer=finalizer):
finalizers = body.get('metadata', {}).get('finalizers', [])
patch.setdefault('metadata', {}).setdefault('finalizers', list(finalizers))
patch['metadata']['finalizers'].append(FINALIZER)
patch['metadata']['finalizers'].append(finalizer)


def allow_deletion(
*,
body: bodies.Body,
patch: patches.Patch,
finalizer: str,
) -> None:
if is_deletion_blocked(body=body):
if is_deletion_blocked(body=body, finalizer=finalizer):
finalizers = body.get('metadata', {}).get('finalizers', [])
patch.setdefault('metadata', {}).setdefault('finalizers', list(finalizers))
if LEGACY_FINALIZER in patch['metadata']['finalizers']:
patch['metadata']['finalizers'].remove(LEGACY_FINALIZER)
if FINALIZER in patch['metadata']['finalizers']:
patch['metadata']['finalizers'].remove(FINALIZER)
if finalizer in patch['metadata']['finalizers']:
patch['metadata']['finalizers'].remove(finalizer)
6 changes: 6 additions & 0 deletions kopf/structs/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ def max_workers(self, value: int) -> None:
@dataclasses.dataclass
class PersistenceSettings:

finalizer: str = 'kopf.zalando.org/KopfFinalizerMarker'
"""
A string marker to be put on a list of finalizers to block the object
from being deleted without framework's/operator's permission.
"""

progress_storage: progress.ProgressStorage = dataclasses.field(
default_factory=progress.SmartProgressStorage)
"""
Expand Down
5 changes: 4 additions & 1 deletion tests/causation/test_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

from kopf.reactor.causation import detect_resource_changing_cause
from kopf.storage.diffbase import LAST_SEEN_ANNOTATION
from kopf.storage.finalizers import FINALIZER
from kopf.structs.bodies import Body
from kopf.structs.handlers import Reason

# Same as in the settings by default.
FINALIZER = 'fin'

# Encoded at runtime, so that we do not make any assumptions on json formatting.
SPEC_DATA = {'spec': {'field': 'value'}}
SPEC_JSON = json.dumps((SPEC_DATA))
Expand Down Expand Up @@ -115,6 +117,7 @@ def content():
@pytest.fixture()
def kwargs():
return dict(
finalizer=FINALIZER,
resource=object(),
logger=object(),
patch=object(),
Expand Down
6 changes: 3 additions & 3 deletions tests/handling/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ async def delete_fn2(**kwargs):


@pytest.fixture()
def cause_mock(mocker, resource):
def cause_mock(mocker, settings, resource):
"""
Mock the resulting _cause_ of the resource change detection logic.
Expand All @@ -183,12 +183,12 @@ def cause_mock(mocker, resource):

# Use everything from a mock, but use the passed `patch` dict as is.
# The event handler passes its own accumulator, and checks/applies it later.
def new_detect_fn(*, diff, new, old, **kwargs):
def new_detect_fn(*, finalizer, diff, new, old, **kwargs):

# For change detection, we ensure that there is no extra cycle of adding a finalizer.
raw_event = kwargs.pop('raw_event', None)
raw_body = raw_event['object']
raw_body.setdefault('metadata', {}).setdefault('finalizers', ['kopf.zalando.org/KopfFinalizerMarker'])
raw_body.setdefault('metadata', {}).setdefault('finalizers', [finalizer])

# Pass through kwargs: resource, logger, patch, diff, old, new.
# I.e. everything except what we mock -- for them, use the mocked values (if not None).
Expand Down
19 changes: 11 additions & 8 deletions tests/handling/daemons/test_daemon_errors.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import logging

import kopf
from kopf.storage.finalizers import FINALIZER


async def test_daemon_stopped_on_permanent_error(
registry, resource, dummy, manual_time,
registry, settings, resource, dummy, manual_time,
caplog, assert_logs, k8s_mocked, simulate_cycle):
caplog.set_level(logging.DEBUG)

Expand All @@ -17,7 +16,8 @@ async def fn(**kwargs):
dummy.steps['called'].set()
raise kopf.PermanentError("boo!")

event_object = {'metadata': {'finalizers': [FINALIZER]}}
finalizer = settings.persistence.finalizer
event_object = {'metadata': {'finalizers': [finalizer]}}
await simulate_cycle(event_object)

await dummy.steps['called'].wait()
Expand All @@ -37,7 +37,7 @@ async def fn(**kwargs):


async def test_daemon_stopped_on_arbitrary_errors_with_mode_permanent(
registry, resource, dummy, manual_time,
registry, settings, resource, dummy, manual_time,
caplog, assert_logs, k8s_mocked, simulate_cycle):
caplog.set_level(logging.DEBUG)

Expand All @@ -49,7 +49,8 @@ async def fn(**kwargs):
dummy.steps['called'].set()
raise Exception("boo!")

event_object = {'metadata': {'finalizers': [FINALIZER]}}
finalizer = settings.persistence.finalizer
event_object = {'metadata': {'finalizers': [finalizer]}}
await simulate_cycle(event_object)

await dummy.steps['called'].wait()
Expand Down Expand Up @@ -83,7 +84,8 @@ async def fn(retry, **kwargs):
else:
dummy.steps['finish'].set()

event_object = {'metadata': {'finalizers': [FINALIZER]}}
finalizer = settings.persistence.finalizer
event_object = {'metadata': {'finalizers': [finalizer]}}
await simulate_cycle(event_object)

await dummy.steps['called'].wait()
Expand All @@ -102,7 +104,7 @@ async def fn(retry, **kwargs):


async def test_daemon_retried_on_arbitrary_error_with_mode_temporary(
registry, resource, dummy,
registry, settings, resource, dummy,
caplog, assert_logs, k8s_mocked, simulate_cycle, manual_time):
caplog.set_level(logging.DEBUG)

Expand All @@ -117,7 +119,8 @@ async def fn(retry, **kwargs):
else:
dummy.steps['finish'].set()

event_object = {'metadata': {'finalizers': [FINALIZER]}}
finalizer = settings.persistence.finalizer
event_object = {'metadata': {'finalizers': [finalizer]}}
await simulate_cycle(event_object)

await dummy.steps['called'].wait()
Expand Down
11 changes: 6 additions & 5 deletions tests/handling/daemons/test_daemon_filtration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import pytest

import kopf
from kopf.storage.finalizers import FINALIZER


# We assume that the handler filtering is tested in details elsewhere (for all handlers).
# Here, we only test if it is applied or not applied.


async def test_daemon_filtration_satisfied(
registry, resource, dummy,
registry, settings, resource, dummy,
caplog, assert_logs, k8s_mocked, simulate_cycle):
caplog.set_level(logging.DEBUG)

Expand All @@ -22,9 +21,10 @@ async def fn(**kwargs):
dummy.kwargs = kwargs
dummy.steps['called'].set()

finalizer = settings.persistence.finalizer
event_body = {'metadata': {'labels': {'a': 'value', 'b': '...'},
'annotations': {'x': 'value', 'y': '...'},
'finalizers': [FINALIZER]}}
'finalizers': [finalizer]}}
await simulate_cycle(event_body)

await dummy.steps['called'].wait()
Expand All @@ -42,7 +42,7 @@ async def fn(**kwargs):
({'a': 'value'}, {'x': 'value', 'y': '...'}),
])
async def test_daemon_filtration_mismatched(
registry, resource, mocker, labels, annotations,
registry, settings, resource, mocker, labels, annotations,
caplog, assert_logs, k8s_mocked, simulate_cycle):
caplog.set_level(logging.DEBUG)
spawn_resource_daemons = mocker.patch('kopf.reactor.daemons.spawn_resource_daemons')
Expand All @@ -53,9 +53,10 @@ async def test_daemon_filtration_mismatched(
async def fn(**kwargs):
pass

finalizer = settings.persistence.finalizer
event_body = {'metadata': {'labels': labels,
'annotations': annotations,
'finalizers': [FINALIZER]}}
'finalizers': [finalizer]}}
await simulate_cycle(event_body)

assert spawn_resource_daemons.called
Expand Down
Loading

0 comments on commit fbc050d

Please sign in to comment.