def owner(request, resource): body = Body(copy.deepcopy(OWNER)) if request.param == 'state-changing-cause': cause = ChangingCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), memo=Memo(), body=body, initial=False, reason=Reason.NOOP, ) with context([(cause_var, cause)]): yield body elif request.param == 'event-watching-cause': cause = WatchingCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), memo=Memo(), body=body, type='irrelevant', event=RawEvent(type='irrelevant', object=OWNER), ) with context([(cause_var, cause)]): yield body else: raise RuntimeError(f"Wrong param for `owner` fixture: {request.param!r}")
def deliver_results( *, outcomes: Mapping[ids.HandlerId, execution.Outcome], patch: patches.Patch, ) -> None: """ Store the results (as returned from the handlers) to the resource. This is not the handlers' state persistence, but the results' persistence. First, the state persistence is stored under ``.status.kopf.progress``, and can (later) be configured to be stored in different fields for different operators operating the same objects: ``.status.kopf.{somename}.progress``. The handlers' result are stored in the top-level ``.status``. Second, the handler results can (also later) be delivered to other objects, e.g. to their owners or label-selected related objects. For this, another class/module will be added. For now, we keep state- and result persistence in one module, but separated. """ for handler_id, outcome in outcomes.items(): if outcome.exception is not None: pass elif outcome.result is None: pass elif isinstance(outcome.result, collections.abc.Mapping): # TODO: merge recursively (patch-merge), do not overwrite the keys if they are present. patch.setdefault('status', {}).setdefault(handler_id, {}).update(outcome.result) else: patch.setdefault('status', {})[handler_id] = copy.deepcopy(outcome.result)
def test_removal_of_the_key(): patch = Patch() patch['xyz'] = None jsonpatch = patch.as_json_patch() assert jsonpatch == [ { 'op': 'remove', 'path': '/xyz' }, ]
def test_removal_of_the_subkey(): patch = Patch() patch['xyz'] = {'abc': None} jsonpatch = patch.as_json_patch() assert jsonpatch == [ { 'op': 'remove', 'path': '/xyz/abc' }, ]
def block_deletion( *, body: bodies.Body, patch: patches.Patch, finalizer: str, ) -> None: 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)
def allow_deletion( *, body: bodies.Body, patch: patches.Patch, finalizer: str, ) -> None: if is_deletion_blocked(body=body, finalizer=finalizer): finalizers = body.get('metadata', {}).get('finalizers', []) patch.setdefault('metadata', {}).setdefault('finalizers', list(finalizers)) if finalizer in patch['metadata']['finalizers']: patch['metadata']['finalizers'].remove(finalizer)
def test_addition_of_the_key(): patch = Patch() patch['xyz'] = 123 jsonpatch = patch.as_json_patch() assert jsonpatch == [ { 'op': 'replace', 'path': '/xyz', 'value': 123 }, ]
def test_addition_of_the_subkey(): body = {'xyz': {'def': 456}} patch = Patch(body=body) patch['xyz'] = {'abc': 123} jsonpatch = patch.as_json_patch() assert jsonpatch == [ { 'op': 'add', 'path': '/xyz/abc', 'value': 123 }, ]
def test_replacement_of_the_subkey(): body = {'xyz': {'abc': 456}} patch = Patch(body=body) patch['xyz'] = {'abc': 123} jsonpatch = patch.as_json_patch() assert jsonpatch == [ { 'op': 'replace', 'path': '/xyz/abc', 'value': 123 }, ]
def test_touching_via_status_storage_with_none_when_absent(cls): storage = cls(touch_field='status.my-dummy') patch = Patch() body = Body({}) storage.touch(body=body, patch=patch, value=None) assert not patch
def test_touching_via_annotations_storage_with_none_when_absent(cls): storage = cls(prefix='my-operator.example.com', touch_key='my-dummy') patch = Patch() body = Body({}) storage.touch(body=body, patch=patch, value=None) assert not patch
def test_daemon_kwargs(resource, attr): body = {'metadata': {'uid': 'uid1', 'name': 'name1', 'namespace': 'ns1', 'labels': {'l1': 'v1'}, 'annotations': {'a1': 'v1'}}, 'spec': {'field': 'value'}, 'status': {'info': 'payload'}} cause = DaemonCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), memo=Memo(), body=Body(body), stopper=DaemonStopper(), ) kwargs = getattr(cause, attr) # cause.kwargs assert set(kwargs) == {'logger', 'resource', 'patch', 'memo', 'body', 'spec', 'status', 'meta', 'uid', 'name', 'namespace', 'labels', 'annotations'} assert kwargs['resource'] is cause.resource assert kwargs['logger'] is cause.logger assert kwargs['patch'] is cause.patch assert kwargs['memo'] is cause.memo assert kwargs['body'] is cause.body assert kwargs['spec'] is cause.body.spec assert kwargs['meta'] is cause.body.metadata assert kwargs['status'] is cause.body.status assert kwargs['labels'] is cause.body.metadata.labels assert kwargs['annotations'] is cause.body.metadata.annotations assert kwargs['uid'] == cause.body.metadata.uid assert kwargs['name'] == cause.body.metadata.name assert kwargs['namespace'] == cause.body.metadata.namespace assert 'stopper' not in kwargs assert 'stopped' not in kwargs
async def test_patching_without_inconsistencies(resource, namespace, settings, caplog, assert_logs, version_api, aresponses, hostname, resp_mocker, patch, response): caplog.set_level(logging.DEBUG) url = resource.get_url(namespace=namespace, name='name1') patch_mock = resp_mocker(return_value=aiohttp.web.json_response(response)) aresponses.add(hostname, url, 'patch', patch_mock) body = Body({'metadata': {'namespace': namespace, 'name': 'name1'}}) logger = LocalObjectLogger(body=body, settings=settings) await patch_and_check( settings=settings, resource=resource, body=body, patch=Patch(patch), logger=logger, ) assert_logs([ "Patching with:", ], prohibited=[ "Patching failed with inconsistencies:", ])
async def test_patching_with_disappearance( resource, namespace, settings, caplog, assert_logs, version_api, aresponses, hostname, resp_mocker): caplog.set_level(logging.DEBUG) patch = {'spec': {'x': 'y'}, 'status': {'s': 't'}} # irrelevant url = resource.get_url(namespace=namespace, name='name1') patch_mock = resp_mocker(return_value=aresponses.Response(status=404)) aresponses.add(hostname, url, 'patch', patch_mock) body = Body({'metadata': {'namespace': namespace, 'name': 'name1'}}) logger = LocalObjectLogger(body=body, settings=settings) await patch_and_check( resource=resource, body=body, patch=Patch(patch), logger=logger, ) assert_logs([ "Patching with:", "Patching was skipped: the object does not exist anymore", ], prohibited=[ "inconsistencies" ])
def test_watching_kwargs(resource, attr): body = {'metadata': {'uid': 'uid1', 'name': 'name1', 'namespace': 'ns1', 'labels': {'l1': 'v1'}, 'annotations': {'a1': 'v1'}}, 'spec': {'field': 'value'}, 'status': {'info': 'payload'}} cause = WatchingCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), memo=Memo(), body=Body(body), type='ADDED', event={'type': 'ADDED', 'object': {}}, ) kwargs = getattr(cause, attr) # cause.kwargs / cause.sync_kwargs / cause.async_kwargs assert set(kwargs) == {'logger', 'resource', 'patch', 'event', 'type', 'memo', 'body', 'spec', 'status', 'meta', 'uid', 'name', 'namespace', 'labels', 'annotations'} assert kwargs['resource'] is cause.resource assert kwargs['logger'] is cause.logger assert kwargs['patch'] is cause.patch assert kwargs['event'] is cause.event assert kwargs['memo'] is cause.memo assert kwargs['type'] is cause.type assert kwargs['body'] is cause.body assert kwargs['spec'] is cause.body.spec assert kwargs['meta'] is cause.body.metadata assert kwargs['status'] is cause.body.status assert kwargs['labels'] is cause.body.metadata.labels assert kwargs['annotations'] is cause.body.metadata.annotations assert kwargs['uid'] == cause.body.metadata.uid assert kwargs['name'] == cause.body.metadata.name assert kwargs['namespace'] == cause.body.metadata.namespace
def test_set_awake_time(storage, handler, expected, body, delay): patch = Patch() state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_handlers([handler]) state = state.with_outcomes(outcomes={handler.id: Outcome(final=False, delay=delay)}) state.store(patch=patch, body=Body(body), storage=storage) assert patch['status']['kopf']['progress']['some-id'].get('delayed') == expected
async def test_special_kwargs_added(fn, resource): body = {'metadata': {'uid': 'uid', 'name': 'name', 'namespace': 'ns'}, 'spec': {'field': 'value'}, 'status': {'info': 'payload'}} # Values can be any. cause = ChangingCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), initial=False, reason=Reason.NOOP, memo=object(), body=Body(body), diff=object(), old=object(), new=object(), ) fn = MagicMock(fn) await invoke(fn, kwargsrc=cause) assert fn.called assert fn.call_count == 1 # Only check that kwargs are passed at all. The exact kwargs per cause are tested separately. assert 'logger' in fn.call_args[1] assert 'resource' in fn.call_args[1]
def test_purging_of_status_storage_nullifies_content(cls): storage = cls(field='status.my-operator') patch = Patch() body = Body({'status': {'my-operator': {'id1': CONTENT_DATA_1}}}) storage.purge(body=body, patch=patch, key=HandlerId('id1')) assert patch assert patch['status']['my-operator']['id1'] is None
def test_started_from_storage_is_preferred_over_from_scratch(storage, handler, body, expected): with freezegun.freeze_time(TS0): state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) with freezegun.freeze_time(TS1): state = state.with_handlers([handler]) patch = Patch() state.store(body=Body({}), patch=patch, storage=storage) assert patch['status']['kopf']['progress']['some-id']['started'] == expected
def test_touching_via_status_storage_with_payload(cls, body_data): storage = cls(field='status.my-operator', touch_field='status.my-dummy') patch = Patch() body = Body(body_data) storage.touch(body=body, patch=patch, value='hello') assert patch assert patch['status']['my-dummy'] == 'hello'
def test_touching_via_status_storage_with_none_when_present(cls): storage = cls(touch_field='status.my-dummy') patch = Patch() body = Body({'status': {'my-dummy': 'something'}}) storage.touch(body=body, patch=patch, value=None) assert patch assert patch['status']['my-dummy'] is None
async def test_status_as_subresource_with_object_fields_only( resp_mocker, aresponses, hostname, resource, namespace): resource = dataclasses.replace(resource, subresources=['status']) # Simulate Kopf's initial state and intention. patch = Patch({'spec': {'x': 'y'}}) # Simulate K8s API's behaviour. Assume something extra is added remotely. object_response = { 'metadata': { 'namespace': 'ns1', 'name': 'name1', 'extra': '123' }, 'spec': { 'x': 'y', 'extra': '456' }, 'status': '...' } status_response = {'status': {'s': 't', 'extra': '789'}} object_url = resource.get_url(namespace=namespace, name='name1') status_url = resource.get_url(namespace=namespace, name='name1', subresource='status') object_patch_mock = resp_mocker( return_value=aiohttp.web.json_response(object_response)) status_patch_mock = resp_mocker( return_value=aiohttp.web.json_response(status_response)) aresponses.add(hostname, object_url, 'patch', object_patch_mock) aresponses.add(hostname, status_url, 'patch', status_patch_mock) reconstructed = await patch_obj(resource=resource, namespace=namespace, name='name1', patch=patch) assert object_patch_mock.called assert object_patch_mock.call_count == 1 assert not status_patch_mock.called data = object_patch_mock.call_args_list[0][0][ 0].data # [callidx][args/kwargs][argidx] assert data == {'spec': {'x': 'y'}} assert reconstructed == { 'metadata': { 'namespace': 'ns1', 'name': 'name1', 'extra': '123' }, 'spec': { 'x': 'y', 'extra': '456' }, 'status': '...' }
def test_touching_via_annotations_storage_with_payload(cls, body_data): storage = cls(prefix='my-operator.example.com', touch_key='my-dummy') patch = Patch() body = Body(body_data) storage.touch(body=body, patch=patch, value='hello') assert patch assert patch['metadata']['annotations'][ 'my-operator.example.com/my-dummy'] == 'hello'
def test_subrefs_ignored_when_not_specified(storage, handler): body = {} patch = Patch() outcome = Outcome(final=True, subrefs=[]) state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_handlers([handler]) state = state.with_outcomes(outcomes={handler.id: outcome}) state.store(patch=patch, body=Body(body), storage=storage) assert patch['status']['kopf']['progress']['some-id']['subrefs'] is None
def test_changing_kwargs(resource, attr): body = { 'metadata': { 'uid': 'uid1', 'name': 'name1', 'namespace': 'ns1', 'labels': { 'l1': 'v1' }, 'annotations': { 'a1': 'v1' } }, 'spec': { 'field': 'value' }, 'status': { 'info': 'payload' } } cause = ChangingCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), initial=False, reason=Reason.NOOP, memo=Memo(), body=Body(body), diff=Diff([]), old=BodyEssence(), new=BodyEssence(), ) kwargs = getattr( cause, attr) # cause.kwargs / cause.sync_kwargs / cause.async_kwargs assert set(kwargs) == { 'logger', 'resource', 'patch', 'reason', 'memo', 'body', 'spec', 'status', 'meta', 'uid', 'name', 'namespace', 'labels', 'annotations', 'diff', 'old', 'new' } assert kwargs['resource'] is cause.resource assert kwargs['reason'] is cause.reason assert kwargs['logger'] is cause.logger assert kwargs['patch'] is cause.patch assert kwargs['memo'] is cause.memo assert kwargs['diff'] is cause.diff assert kwargs['old'] is cause.old assert kwargs['new'] is cause.new assert kwargs['body'] is cause.body assert kwargs['spec'] is cause.body.spec assert kwargs['meta'] is cause.body.metadata assert kwargs['status'] is cause.body.status assert kwargs['labels'] is cause.body.metadata.labels assert kwargs['annotations'] is cause.body.metadata.annotations assert kwargs['uid'] == cause.body.metadata.uid assert kwargs['name'] == cause.body.metadata.name assert kwargs['namespace'] == cause.body.metadata.namespace
def test_subrefs_added_to_preexisting_subrefs(storage, handler): body = {'status': {'kopf': {'progress': {'some-id': {'subrefs': ['sub9/2', 'sub9/1']}}}}} patch = Patch() outcome_subrefs = ['sub2/b', 'sub2/a', 'sub2', 'sub1', 'sub3'] expected_subrefs = ['sub1', 'sub2', 'sub2/a', 'sub2/b', 'sub3', 'sub9/1', 'sub9/2'] outcome = Outcome(final=True, subrefs=outcome_subrefs) state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_handlers([handler]) state = state.with_outcomes(outcomes={handler.id: outcome}) state.store(patch=patch, body=Body(body), storage=storage) assert patch['status']['kopf']['progress']['some-id']['subrefs'] == expected_subrefs
def test_storing_to_status_storage_populates_keys(cls): storage = cls(field='status.my-operator') patch = Patch() body = Body({}) storage.store(body=body, patch=patch, key=HandlerId('id1'), record=CONTENT_DATA_1) assert patch assert patch['status']['my-operator']['id1'] == CONTENT_DATA_1
def test_store_success(storage, handler, expected_retries, expected_stopped, body): patch = Patch() state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_handlers([handler]) state = state.with_outcomes(outcomes={handler.id: Outcome(final=True)}) state.store(patch=patch, body=Body(body), storage=storage) assert patch['status']['kopf']['progress']['some-id']['success'] is True assert patch['status']['kopf']['progress']['some-id']['failure'] is False assert patch['status']['kopf']['progress']['some-id']['retries'] == expected_retries assert patch['status']['kopf']['progress']['some-id']['stopped'] == expected_stopped assert patch['status']['kopf']['progress']['some-id']['message'] is None
def test_admission_kwargs(resource, attr): body = {'metadata': {'uid': 'uid1', 'name': 'name1', 'namespace': 'ns1', 'labels': {'l1': 'v1'}, 'annotations': {'a1': 'v1'}}, 'spec': {'field': 'value'}, 'status': {'info': 'payload'}} cause = WebhookCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=OperatorIndexers().indices, resource=resource, patch=Patch(), memo=Memo(), body=Body(body), dryrun=False, headers={'k1': 'v1'}, sslpeer={'k2': 'v2'}, userinfo={'k3': 'v3'}, warnings=['w1'], webhook=None, reason=None, operation=None, subresource=None, new=BodyEssence(body), old=None, diff=diffs.diff(BodyEssence(body), None), ) kwargs = getattr(cause, attr) # cause.kwargs / cause.sync_kwargs / cause.async_kwargs assert set(kwargs) == {'logger', 'resource', 'dryrun', 'headers', 'sslpeer', 'userinfo', 'warnings', 'subresource', 'patch', 'memo', 'body', 'spec', 'status', 'meta', 'uid', 'name', 'namespace', 'labels', 'annotations', 'old', 'new', 'diff', 'operation'} assert kwargs['resource'] is cause.resource assert kwargs['logger'] is cause.logger assert kwargs['dryrun'] is cause.dryrun assert kwargs['headers'] is cause.headers assert kwargs['sslpeer'] is cause.sslpeer assert kwargs['userinfo'] is cause.userinfo assert kwargs['warnings'] is cause.warnings assert kwargs['patch'] is cause.patch assert kwargs['memo'] is cause.memo assert kwargs['body'] is cause.body assert kwargs['spec'] is cause.body.spec assert kwargs['meta'] is cause.body.metadata assert kwargs['status'] is cause.body.status assert kwargs['labels'] is cause.body.metadata.labels assert kwargs['annotations'] is cause.body.metadata.annotations assert kwargs['uid'] == cause.body.metadata.uid assert kwargs['name'] == cause.body.metadata.name assert kwargs['namespace'] == cause.body.metadata.namespace assert kwargs['operation'] == cause.operation assert kwargs['new'] == cause.new assert kwargs['old'] == cause.old assert kwargs['diff'] == cause.diff
def test_storing_to_annotations_storage_populates_keys(cls): storage = cls(prefix='my-operator.example.com', verbose=True) patch = Patch() body = Body({}) storage.store(body=body, patch=patch, key=HandlerId('id1'), record=CONTENT_DATA_1) assert patch assert patch['metadata']['annotations'][ 'my-operator.example.com/id1'] == CONTENT_JSON_1