def test_on_cleanup_with_all_kwargs(mocker): registry = OperatorRegistry() @kopf.on.cleanup(id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) def fn(**_): pass handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.CLEANUP assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78
def test_requires_finalizer_no_deletion_handler(cause_factory): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, body=OBJECT_BODY) @kopf.on.create('group', 'version', 'plural', registry=registry) def fn1(**_): pass requires_finalizer = registry.resource_changing_handlers.requires_finalizer( cause) assert requires_finalizer is False
def test_on_probe_with_all_kwargs(mocker): registry = OperatorRegistry() @kopf.on.probe(id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, cooldown=78) def fn(**_): pass handlers = registry.get_activity_handlers(activity=Activity.PROBE) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.PROBE assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].cooldown == 78
def test_requires_finalizer_multiple_handlers(optional, expected): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') @kopf.on.create('group', 'version', 'plural', registry=registry) def fn1(**_): pass @kopf.on.delete('group', 'version', 'plural', registry=registry, optional=optional) def fn2(**_): pass with pytest.deprecated_call( match=r"use registry.resource_changing_handlers"): requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected
async def test_errors_are_raised_aggregated(activity): def sample_fn1(**_): raise PermanentError("boo!123") def sample_fn2(**_): raise PermanentError("boo!456") registry = OperatorRegistry() registry.register_activity_handler(fn=sample_fn1, id='id1', activity=activity) registry.register_activity_handler(fn=sample_fn2, id='id2', activity=activity) with pytest.raises(ActivityError) as e: await run_activity( registry=registry, activity=activity, lifecycle=all_at_once, ) assert set(e.value.outcomes.keys()) == {'id1', 'id2'} assert e.value.outcomes['id1'].final assert e.value.outcomes['id1'].delay is None assert e.value.outcomes['id1'].result is None assert e.value.outcomes['id1'].exception is not None assert e.value.outcomes['id2'].final assert e.value.outcomes['id2'].delay is None assert e.value.outcomes['id2'].result is None assert e.value.outcomes['id2'].exception is not None assert str(e.value.outcomes['id1'].exception) == "boo!123" assert str(e.value.outcomes['id2'].exception) == "boo!456"
def test_on_delete_with_all_kwargs(mocker, optional): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = mocker.MagicMock(resource=resource, reason=Reason.DELETE) mocker.patch('kopf.reactor.registries.match', return_value=True) when = lambda **_: False @kopf.on.delete('group', 'version', 'plural', id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, optional=optional, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}, when=when) def fn(**_): pass with pytest.deprecated_call( match=r"use registry.resource_changing_handlers"): handlers = registry.get_resource_changing_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.DELETE assert handlers[0].field is None assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'} assert handlers[0].when == when
def test_on_field_with_all_kwargs(mocker): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') diff = [('op', ('field', 'subfield'), 'old', 'new')] cause = mocker.MagicMock(resource=resource, reason=Reason.UPDATE, diff=diff) mocker.patch('kopf.reactor.registries.match', return_value=True) when = lambda **_: False @kopf.on.field('group', 'version', 'plural', 'field.subfield', id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}, when=when) def fn(**_): pass handlers = registry.get_resource_changing_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None assert handlers[0].field == ('field', 'subfield') assert handlers[0].id == 'id/field.subfield' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'} assert handlers[0].when == when
async def test_errors_are_raised_aggregated(settings, activity): def sample_fn1(**_): raise PermanentError("boo!123") def sample_fn2(**_): raise PermanentError("boo!456") registry = OperatorRegistry() registry._activities.append( ActivityHandler( fn=sample_fn1, id='id1', activity=activity, param=None, errors=None, timeout=None, retries=None, backoff=None, )) registry._activities.append( ActivityHandler( fn=sample_fn2, id='id2', activity=activity, param=None, errors=None, timeout=None, retries=None, backoff=None, )) with pytest.raises(ActivityError) as e: await run_activity( registry=registry, settings=settings, activity=activity, lifecycle=all_at_once, indices=OperatorIndexers().indices, memo=Memo(), ) assert set(e.value.outcomes.keys()) == {'id1', 'id2'} assert e.value.outcomes['id1'].final assert e.value.outcomes['id1'].delay is None assert e.value.outcomes['id1'].result is None assert e.value.outcomes['id1'].exception is not None assert e.value.outcomes['id2'].final assert e.value.outcomes['id2'].delay is None assert e.value.outcomes['id2'].result is None assert e.value.outcomes['id2'].exception is not None assert str(e.value.outcomes['id1'].exception) == "boo!123" assert str(e.value.outcomes['id2'].exception) == "boo!456"
async def activity_trigger( *, lifecycle: lifecycles.LifeCycleFn, registry: registries.OperatorRegistry, activity: causation.Activity, ) -> Mapping[registries.HandlerId, registries.HandlerResult]: """ Execute a handling cycle until succeeded or permanently failed. This mimics the behaviour of patching-watching in Kubernetes, but in-memory. """ logger = logging.getLogger(f'kopf.activities.{activity.value}') # For the activity handlers, we have neither bodies, nor patches, just the state. cause = causation.ActivityCause(logger=logger, activity=activity) handlers = registry.get_activity_handlers(activity=activity) state = states.State.from_scratch(handlers=handlers) latest_outcomes: MutableMapping[registries.HandlerId, states.HandlerOutcome] = {} while not state.done: outcomes = await _execute_handlers( lifecycle=lifecycle, handlers=handlers, cause=cause, state=state, ) latest_outcomes.update(outcomes) state = state.with_outcomes(outcomes) delay = state.delay if delay: await sleeping.sleep_or_wait( min(delay, WAITING_KEEPALIVE_INTERVAL), asyncio.Event()) # Activities assume that all handlers must eventually succeed. # We raise from the 1st exception only: just to have something real in the tracebacks. # For multiple handlers' errors, the logs should be investigated instead. exceptions = [ outcome.exception for outcome in latest_outcomes.values() if outcome.exception is not None ] if exceptions: raise ActivityError("One or more handlers failed.", outcomes=latest_outcomes) \ from exceptions[0] # If nothing has failed, we return identifiable results. The outcomes/states are internal. # The order of results is not guaranteed (the handlers can succeed on one of the retries). results = { handler_id: outcome.result for handler_id, outcome in latest_outcomes.items() if outcome.result is not None } return results
async def test_empty_registry_produces_no_credentials(): vault = Vault() registry = OperatorRegistry() await authenticate( registry=registry, vault=vault, ) assert not vault with pytest.raises(LoginError): async for _, _ in vault: pass
def test_requires_finalizer_multiple_handlers(optional, expected, cause_factory): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, body=OBJECT_BODY) @kopf.on.create('group', 'version', 'plural', registry=registry) def fn1(**_): pass @kopf.on.delete('group', 'version', 'plural', registry=registry, optional=optional) def fn2(**_): pass with pytest.deprecated_call(match=r"cease using the internal registries"): requires_finalizer = registry.requires_finalizer(resource=resource, cause=cause) assert requires_finalizer == expected
def test_registry_and_settings_are_propagated(mocker): operator_mock = mocker.patch('kopf.reactor.running.operator') registry = OperatorRegistry() settings = OperatorSettings() with KopfRunner(['run', '--standalone'], registry=registry, settings=settings) as runner: pass assert runner.exit_code == 0 assert runner.exception is None assert operator_mock.called assert operator_mock.call_args[1]['registry'] is registry assert operator_mock.call_args[1]['settings'] is settings
async def test_retries_are_simulated(activity, mocker): mock = mocker.MagicMock() def sample_fn(**_): mock() raise TemporaryError('to be retried', delay=0) registry = OperatorRegistry() registry.register_activity_handler(fn=sample_fn, id='id', activity=activity, retries=3) with pytest.raises(ActivityError) as e: await run_activity( registry=registry, activity=activity, lifecycle=all_at_once, ) assert isinstance(e.value.outcomes['id'].exception, PermanentError) assert mock.call_count == 3
def test_on_resume_with_all_kwargs(mocker, reason): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=False) mocker.patch('kopf.reactor.registries.match', return_value=True) @kopf.on.resume('group', 'version', 'plural', id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, deleted=True, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}) def fn(**_): pass handlers = registry.get_resource_changing_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None assert handlers[0].field is None assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].cooldown == 78 # deprecated alias assert handlers[0].deleted == True assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'}
def test_on_startup_with_all_kwargs(mocker): registry = OperatorRegistry() @kopf.on.startup(id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) def fn(**_): pass with pytest.deprecated_call(match=r"cease using the internal registries"): handlers = registry.get_activity_handlers(activity=Activity.STARTUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.STARTUP assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78
async def test_noreturn_handler_produces_no_credentials(): vault = Vault() registry = OperatorRegistry() def login_fn(**_): pass registry.register_activity_handler( fn=login_fn, id= 'login_fn', # auto-detection does not work, as it is local to the test function. activity=Activity.AUTHENTICATION, ) await authenticate( registry=registry, vault=vault, ) assert not vault with pytest.raises(LoginError): async for _, _ in vault: pass
def test_on_probe_with_all_kwargs(mocker): registry = OperatorRegistry() @kopf.on.probe(id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) def fn(**_): pass with pytest.deprecated_call(match=r"use registry.activity_handlers"): handlers = registry.get_activity_handlers(activity=Activity.PROBE) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.PROBE assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78
def test_requires_finalizer_deletion_handler(optional, expected): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') @kopf.on.delete('group', 'version', 'plural', registry=registry, optional=optional) def fn(**_): pass requires_finalizer = registry.resource_changing_handlers[ resource].requires_finalizer(CAUSE) assert requires_finalizer == expected
async def test_empty_registry_produces_no_credentials(settings): vault = Vault() registry = OperatorRegistry() await authenticate( registry=registry, settings=settings, vault=vault, memo=Memo(), indices=OperatorIndexers().indices, ) assert not vault with pytest.raises(LoginError): async for _, _ in vault: pass
def test_on_field_with_most_kwargs(mocker, cause_factory): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') old = {'field': {'subfield': 'old'}} new = {'field': {'subfield': 'new'}} cause = cause_factory(resource=resource, reason=Reason.UPDATE, old=old, new=new, body=new) mocker.patch('kopf.reactor.registries.match', return_value=True) when = lambda **_: False @kopf.on.field('group', 'version', 'plural', field='field.subfield', id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}, when=when) def fn(**_): pass handlers = registry.resource_changing_handlers[resource].get_handlers( cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None assert handlers[0].id == 'id/field.subfield' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'} assert handlers[0].when == when assert handlers[0].field == ('field', 'subfield') assert handlers[0].value is None assert handlers[0].old is None assert handlers[0].new is None
def test_requires_finalizer_deletion_handler(optional, expected, cause_factory): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, body=OBJECT_BODY) @kopf.on.delete('group', 'version', 'plural', registry=registry, optional=optional) def fn(**_): pass requires_finalizer = registry.resource_changing_handlers.requires_finalizer( cause) assert requires_finalizer == expected
async def test_results_are_returned_on_success(settings, activity): def sample_fn1(**_): return 123 def sample_fn2(**_): return 456 registry = OperatorRegistry() registry._activities.append( ActivityHandler( fn=sample_fn1, id='id1', activity=activity, param=None, errors=None, timeout=None, retries=None, backoff=None, )) registry._activities.append( ActivityHandler( fn=sample_fn2, id='id2', activity=activity, param=None, errors=None, timeout=None, retries=None, backoff=None, )) results = await run_activity( registry=registry, settings=settings, activity=activity, lifecycle=all_at_once, indices=OperatorIndexers().indices, memo=Memo(), ) assert set(results.keys()) == {'id1', 'id2'} assert results['id1'] == 123 assert results['id2'] == 456
def test_resources(): handler = Mock() resource1 = Resource('group1', 'version1', 'plural1') resource2 = Resource('group2', 'version2', 'plural2') registry = OperatorRegistry() registry.resource_watching_handlers[resource1].append(handler) registry.resource_changing_handlers[resource2].append(handler) registry.resource_watching_handlers[resource2].append(handler) registry.resource_changing_handlers[resource1].append(handler) resources = registry.resources assert isinstance(resources, collections.abc.Collection) assert len(resources) == 2 assert resource1 in resources assert resource2 in resources
def test_on_resume_with_most_kwargs(mocker, reason, cause_factory): registry = OperatorRegistry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, reason=reason, initial=True) mocker.patch('kopf.reactor.registries.match', return_value=True) when = lambda **_: False @kopf.on.resume('group', 'version', 'plural', id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, deleted=True, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}, when=when) def fn(**_): pass handlers = registry.resource_changing_handlers[resource].get_handlers( cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None assert handlers[0].id == 'id' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].deleted == True assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'} assert handlers[0].when == when assert handlers[0].field is None assert handlers[0].value is None assert handlers[0].old is None assert handlers[0].new is None
def test_resources(): resource1 = Resource('group1', 'version1', 'plural1') resource2 = Resource('group2', 'version2', 'plural2') selector1 = Selector('group1', 'version1', 'plural1') selector2 = Selector('group2', 'version2', 'plural2') handler1 = Mock(selector=selector1) handler2 = Mock(selector=selector2) registry = OperatorRegistry() registry.resource_watching_handlers.append(handler1) registry.resource_changing_handlers.append(handler2) registry.resource_watching_handlers.append(handler2) registry.resource_changing_handlers.append(handler1) resources = registry.resources assert isinstance(resources, collections.abc.Collection) assert len(resources) == 2 assert resource1 in resources assert resource2 in resources
async def test_errors_are_cascaded_from_one_of_the_originals(activity): def sample_fn(**_): raise PermanentError("boo!") registry = OperatorRegistry() registry.activity_handlers.append(ActivityHandler( fn=sample_fn, id='id', activity=activity, errors=None, timeout=None, retries=None, backoff=None, cooldown=None, )) with pytest.raises(ActivityError) as e: await run_activity( registry=registry, activity=activity, lifecycle=all_at_once, ) assert e.value.__cause__ assert type(e.value.__cause__) is PermanentError assert str(e.value.__cause__) == "boo!"
def test_resources(): registry = OperatorRegistry() registry.register_resource_changing_handler('group1', 'version1', 'plural1', some_fn) registry.register_resource_changing_handler('group2', 'version2', 'plural2', some_fn) resources = registry.resources assert isinstance(resources, collections.abc.Collection) assert len(resources) == 2 resource1 = Resource('group1', 'version1', 'plural1') resource2 = Resource('group2', 'version2', 'plural2') assert resource1 in resources assert resource2 in resources
def test_on_delete_with_most_kwargs(mocker, cause_factory, optional, resource): registry = OperatorRegistry() cause = cause_factory(resource=resource, reason=Reason.DELETE) mocker.patch('kopf.reactor.registries.match', return_value=True) when = lambda **_: False @kopf.on.delete(*resource, id='id', registry=registry, errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, optional=optional, field='field.subfield', value=999, labels={'somelabel': 'somevalue'}, annotations={'someanno': 'somevalue'}, when=when) def fn(**_): pass handlers = registry._resource_changing.get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.DELETE assert handlers[0].id == 'id/field.subfield' assert handlers[0].errors == ErrorsMode.PERMANENT assert handlers[0].timeout == 123 assert handlers[0].retries == 456 assert handlers[0].backoff == 78 assert handlers[0].labels == {'somelabel': 'somevalue'} assert handlers[0].annotations == {'someanno': 'somevalue'} assert handlers[0].when == when assert handlers[0].field == ('field', 'subfield') assert handlers[0].value == 999 assert handlers[0].old is None assert handlers[0].new is None