Exemplo n.º 1
0
def test_remove_finalizers_when_empty():
    body = {}
    patch = {}
    allow_deletion(body=body, patch=patch, finalizer='fin')
    assert patch == {}
Exemplo n.º 2
0
def test_remove_finalizers_when_absent():
    body = {'metadata': {'finalizers': ['other1', 'other2']}}
    patch = {}
    allow_deletion(body=body, patch=patch, finalizer='fin')
    assert patch == {}
Exemplo n.º 3
0
async def process_resource_causes(
    lifecycle: lifecycles.LifeCycleFn,
    indexers: indexing.OperatorIndexers,
    registry: registries.OperatorRegistry,
    settings: configuration.OperatorSettings,
    resource: references.Resource,
    raw_event: bodies.RawEvent,
    body: bodies.Body,
    patch: patches.Patch,
    logger: loggers.ObjectLogger,
    memory: containers.ResourceMemory,
) -> Tuple[Collection[float], bool]:

    finalizer = settings.persistence.finalizer
    extra_fields = (
        # NB: indexing handlers are useless here, they are handled on their own.
        registry._resource_watching.get_extra_fields(resource=resource)
        | registry._resource_changing.get_extra_fields(resource=resource)
        | registry._resource_spawning.get_extra_fields(resource=resource))
    old = settings.persistence.diffbase_storage.fetch(body=body)
    new = settings.persistence.diffbase_storage.build(
        body=body, extra_fields=extra_fields)
    old = settings.persistence.progress_storage.clear(
        essence=old) if old is not None else None
    new = settings.persistence.progress_storage.clear(
        essence=new) if new is not None else None
    diff = diffs.diff(old, new)

    # Detect what are we going to do on this processing cycle.
    resource_watching_cause = causation.detect_resource_watching_cause(
        raw_event=raw_event,
        resource=resource,
        indices=indexers.indices,
        logger=logger,
        patch=patch,
        body=body,
        memo=memory.memo,
    ) if registry._resource_watching.has_handlers(resource=resource) else None

    resource_spawning_cause = causation.detect_resource_spawning_cause(
        resource=resource,
        indices=indexers.indices,
        logger=logger,
        patch=patch,
        body=body,
        memo=memory.memo,
        reset=bool(
            diff),  # only essential changes reset idling, not every event
    ) if registry._resource_spawning.has_handlers(resource=resource) else None

    resource_changing_cause = causation.detect_resource_changing_cause(
        finalizer=finalizer,
        raw_event=raw_event,
        resource=resource,
        indices=indexers.indices,
        logger=logger,
        patch=patch,
        body=body,
        old=old,
        new=new,
        diff=diff,
        memo=memory.memo,
        initial=memory.noticed_by_listing and not memory.fully_handled_once,
    ) if registry._resource_changing.has_handlers(resource=resource) else None

    # If there are any handlers for this resource kind in general, but not for this specific object
    # due to filters, then be blind to it, store no state, and log nothing about the handling cycle.
    if (resource_changing_cause is not None
            and not registry._resource_changing.prematch(
                cause=resource_changing_cause)):
        resource_changing_cause = None

    # Block the object from deletion if we have anything to do in its end of life:
    # specifically, if there are daemons to kill or mandatory on-deletion handlers to call.
    # 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,
                                                         finalizer=finalizer)
    deletion_must_be_blocked = (
        (resource_spawning_cause is not None
         and registry._resource_spawning.requires_finalizer(
             cause=resource_spawning_cause,
             excluded=memory.forever_stopped,
         )) or (resource_changing_cause is not None
                and registry._resource_changing.requires_finalizer(
                    cause=resource_changing_cause, )))

    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, 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, 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.
    # The low-level spies go ASAP always. However, the daemons are spawned before the high-level
    # handlers and killed after them: the daemons should live throughout the full object lifecycle.
    if resource_watching_cause is not None:
        await process_resource_watching_cause(
            lifecycle=lifecycles.all_at_once,
            registry=registry,
            settings=settings,
            cause=resource_watching_cause,
        )

    resource_spawning_delays: Collection[float] = []
    if resource_spawning_cause is not None:
        resource_spawning_delays = await process_resource_spawning_cause(
            registry=registry,
            settings=settings,
            memory=memory,
            cause=resource_spawning_cause,
        )

    resource_changing_delays: Collection[float] = []
    if resource_changing_cause is not None:
        resource_changing_delays = await process_resource_changing_cause(
            lifecycle=lifecycle,
            registry=registry,
            settings=settings,
            memory=memory,
            cause=resource_changing_cause,
        )

    # Release the object if everything is done, and it is marked for deletion.
    # But not when it has already gone.
    if deletion_is_ongoing and deletion_is_blocked \
            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, finalizer=finalizer)

    delays = list(resource_spawning_delays) + list(resource_changing_delays)
    return (delays, resource_changing_cause is not None)
Exemplo n.º 4
0
def test_remove_finalizers_keeps_others(finalizer):
    body = {'metadata': {'finalizers': ['other1', finalizer, 'other2']}}
    patch = {}
    allow_deletion(body=body, patch=patch, finalizer='fin')
    assert patch == {'metadata': {'finalizers': ['other1', 'other2']}}
Exemplo n.º 5
0
async def process_resource_event(
    lifecycle: lifecycles.LifeCycleFn,
    registry: registries.OperatorRegistry,
    settings: configuration.OperatorSettings,
    memories: containers.ResourceMemories,
    resource: resources.Resource,
    raw_event: bodies.RawEvent,
    replenished: asyncio.Event,
    event_queue: posting.K8sEventQueue,
) -> None:
    """
    Handle a single custom object low-level watch-event.

    Convert the low-level events, as provided by the watching/queueing tasks,
    to the high-level causes, and then call the cause-handling logic.

    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).
    raw_type, raw_body = raw_event['type'], raw_event['object']
    memory = await memories.recall(raw_body,
                                   noticed_by_listing=raw_type is None)
    if memory.live_fresh_body is not None:
        memory.live_fresh_body._replace_with(raw_body)
    if raw_type == 'DELETED':
        await memories.forget(raw_body)

    # Convert to a heavy mapping-view wrapper only now, when heavy processing begins.
    # Raw-event streaming, queueing, and batching use regular lightweight dicts.
    # Why here? 1. Before it splits into multiple causes & handlers for the same object's body;
    # 2. After it is batched (queueing); 3. While the "raw" parsed JSON is still known;
    # 4. Same as where a patch object of a similar wrapping semantics is created.
    body = memory.live_fresh_body if memory.live_fresh_body is not None else bodies.Body(
        raw_body)
    patch = patches.Patch()

    # Each object has its own prefixed logger, to distinguish parallel handling.
    logger = logging_engine.ObjectLogger(body=body, settings=settings)
    posting.event_queue_loop_var.set(asyncio.get_running_loop())
    posting.event_queue_var.set(
        event_queue)  # till the end of this object's task.

    extra_fields = registry.resource_changing_handlers[
        resource].get_extra_fields()
    old = settings.persistence.diffbase_storage.fetch(body=body)
    new = settings.persistence.diffbase_storage.build(
        body=body, extra_fields=extra_fields)
    old = settings.persistence.progress_storage.clear(
        essence=old) if old is not None else None
    new = settings.persistence.progress_storage.clear(
        essence=new) if new is not None else None
    diff = diffs.diff(old, new)

    # Detect what are we going to do on this processing cycle.
    resource_watching_cause = causation.detect_resource_watching_cause(
        raw_event=raw_event,
        resource=resource,
        logger=logger,
        patch=patch,
        body=body,
        memo=memory.memo,
    ) if registry.resource_watching_handlers[resource] else None

    resource_spawning_cause = causation.detect_resource_spawning_cause(
        resource=resource,
        logger=logger,
        patch=patch,
        body=body,
        memo=memory.memo,
        reset=bool(
            diff),  # only essential changes reset idling, not every 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,
        patch=patch,
        body=body,
        old=old,
        new=new,
        diff=diff,
        memo=memory.memo,
        initial=memory.noticed_by_listing and not memory.fully_handled_once,
    ) if registry.resource_changing_handlers[resource] else None

    # Block the object from deletion if we have anything to do in its end of life:
    # specifically, if there are daemons to kill or mandatory on-deletion handlers to call.
    # 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,
                                                         finalizer=finalizer)
    deletion_must_be_blocked = (
        (resource_spawning_cause is not None
         and registry.resource_spawning_handlers[resource].requires_finalizer(
             cause=resource_spawning_cause,
             excluded=memory.forever_stopped,
         )) or
        (resource_changing_cause is not None
         and registry.resource_changing_handlers[resource].requires_finalizer(
             cause=resource_changing_cause, )))

    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, 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, 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.
    # The low-level spies go ASAP always. However, the daemons are spawned before the high-level
    # handlers and killed after them: the daemons should live throughout the full object lifecycle.
    if resource_watching_cause is not None:
        await process_resource_watching_cause(
            lifecycle=lifecycles.all_at_once,
            registry=registry,
            settings=settings,
            cause=resource_watching_cause,
        )

    resource_spawning_delays: Collection[float] = []
    if resource_spawning_cause is not None:
        resource_spawning_delays = await process_resource_spawning_cause(
            registry=registry,
            settings=settings,
            memory=memory,
            cause=resource_spawning_cause,
        )

    resource_changing_delays: Collection[float] = []
    if resource_changing_cause is not None:
        resource_changing_delays = await process_resource_changing_cause(
            lifecycle=lifecycle,
            registry=registry,
            settings=settings,
            memory=memory,
            cause=resource_changing_cause,
        )

    # Release the object if everything is done, and it is marked for deletion.
    # But not when it has already gone.
    if deletion_is_ongoing and deletion_is_blocked \
            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, 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.
    # And only if the object is at least supposed to exist (not "GONE"), even if actually does not.
    if raw_event['type'] != 'DELETED':
        await apply_reaction_outcomes(
            settings=settings,
            resource=resource,
            body=body,
            patch=patch,
            logger=logger,
            delays=list(resource_spawning_delays) +
            list(resource_changing_delays),
            replenished=replenished,
        )