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
def test_removal_identifies_by_uid(): owner1 = copy.deepcopy(OWNER) owner2 = copy.deepcopy(OWNER) owner3 = copy.deepcopy(OWNER) owner1['kind'] = 'KindA' owner2['kind'] = 'KindA' owner3['kind'] = 'KindB' owner1['metadata']['name'] = 'name-a' owner2['metadata']['name'] = 'name-b' owner3['metadata']['name'] = 'name-b' owner1['metadata']['uid'] = 'uid-0' owner2['metadata']['uid'] = 'uid-0' owner3['metadata']['uid'] = 'uid-0' obj = {} # Three different owners added, but all have the same uid. # One is removed and only once, all must be gone (due to same uids). kopf.append_owner_reference( obj, owner=Body(owner1)) # assumed to work, tested above kopf.append_owner_reference( obj, owner=Body(owner2)) # assumed to work, tested above kopf.append_owner_reference( obj, owner=Body(owner3)) # assumed to work, tested above kopf.remove_owner_reference( obj, owner=Body(owner1)) # this one is being tested here assert len(obj['metadata']['ownerReferences']) == 0
def test_adopting(mocker, forced, strict, nested): # These methods are tested in their own tests. # We just check that they are called at all. append_owner_ref = mocker.patch( 'kopf._kits.hierarchies.append_owner_reference') harmonize_naming = mocker.patch('kopf._kits.hierarchies.harmonize_naming') adjust_namespace = mocker.patch('kopf._kits.hierarchies.adjust_namespace') label = mocker.patch('kopf._kits.hierarchies.label') obj = {} kopf.adopt(obj, owner=Body(OWNER), forced=forced, strict=strict, nested=nested) assert append_owner_ref.called assert harmonize_naming.called assert adjust_namespace.called assert label.called assert append_owner_ref.call_args == call(obj, owner=Body(OWNER)) assert harmonize_naming.call_args == call(obj, name=OWNER_NAME, forced=forced, strict=strict) assert adjust_namespace.call_args == call(obj, namespace=OWNER_NAMESPACE, forced=forced) assert label.call_args == call(obj, labels=OWNER_LABELS, nested=nested, forced=forced)
def test_appending_deduplicates_by_uid(): """ The uid is the only necessary criterion to identify same objects. No matter how we change the irrelevant non-id fields, they must be ignored. """ owner1 = copy.deepcopy(OWNER) owner2 = copy.deepcopy(OWNER) owner3 = copy.deepcopy(OWNER) owner1['kind'] = 'KindA' owner2['kind'] = 'KindA' owner3['kind'] = 'KindB' owner1['metadata']['name'] = 'name-a' owner2['metadata']['name'] = 'name-b' owner3['metadata']['name'] = 'name-b' owner1['metadata']['uid'] = 'uid-0' owner2['metadata']['uid'] = 'uid-0' owner3['metadata']['uid'] = 'uid-0' obj = {} kopf.append_owner_reference(obj, owner=Body(owner1)) kopf.append_owner_reference(obj, owner=Body(owner2)) kopf.append_owner_reference(obj, owner=Body(owner3)) assert len(obj['metadata']['ownerReferences']) == 1 assert obj['metadata']['ownerReferences'][0]['uid'] == 'uid-0'
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_removal_from_kubernetes_model(kubernetes_model): kubernetes_model.metadata = None kopf.append_owner_reference(kubernetes_model, owner=Body(OWNER)) kopf.remove_owner_reference(kubernetes_model, owner=Body(OWNER)) assert kubernetes_model.metadata is not None assert kubernetes_model.metadata.owner_references is not None assert isinstance(kubernetes_model.metadata.owner_references, list) assert len(kubernetes_model.metadata.owner_references) == 0
def test_removal_from_pykube_object(pykube_object): del pykube_object.obj['metadata'] kopf.append_owner_reference(pykube_object, owner=Body(OWNER)) kopf.remove_owner_reference(pykube_object, owner=Body(OWNER)) assert 'metadata' in pykube_object.obj assert 'ownerReferences' in pykube_object.obj['metadata'] assert isinstance(pykube_object.obj['metadata']['ownerReferences'], list) assert len(pykube_object.obj['metadata']['ownerReferences']) == 0
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_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_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_removal_from_dict(): obj = {} kopf.append_owner_reference( obj, owner=Body(OWNER)) # assumed to work, tested above kopf.remove_owner_reference( obj, owner=Body(OWNER)) # this one is being tested here assert 'metadata' in obj assert 'ownerReferences' in obj['metadata'] assert isinstance(obj['metadata']['ownerReferences'], list) assert len(obj['metadata']['ownerReferences']) == 0
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
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:", ])
def test_appending_distinguishes_by_uid(): """ Changing only the uid should be sufficient to consider a new owner. Here, all other non-id fields are the same, and must be ignored. """ owner1 = copy.deepcopy(OWNER) owner2 = copy.deepcopy(OWNER) owner1['metadata']['uid'] = 'uid-a' owner2['metadata']['uid'] = 'uid-b' obj = {} kopf.append_owner_reference(obj, owner=Body(owner1)) kopf.append_owner_reference(obj, owner=Body(owner2)) uids = {ref['uid'] for ref in obj['metadata']['ownerReferences']} assert uids == {'uid-a', 'uid-b'}
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
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 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
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]
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_issue_601_deletion_supersedes_other_processing(storage, reason): body = {'status': {'kopf': {'progress': { 'fn1': {'purpose': reason.value, 'failure': True}, 'fn2': {'purpose': reason.value, 'success': True}, 'fn3': {'purpose': reason.value, 'delayed': TS1_ISO}, }}}} create_handler1 = Mock(id='fn1', spec_set=['id']) create_handler2 = Mock(id='fn2', spec_set=['id']) create_handler3 = Mock(id='fn3', spec_set=['id']) delete_handler9 = Mock(id='delete_fn', spec_set=['id']) owned_handlers = [create_handler1, create_handler2, create_handler3, delete_handler9] cause_handlers = [delete_handler9] state = State.from_storage(body=Body(body), handlers=owned_handlers, storage=storage) state = state.with_purpose(Reason.DELETE) state = state.with_handlers(cause_handlers) assert len(state) == 4 assert state.extras == {reason: StateCounters(success=1, failure=1, running=1)} assert state.counts == StateCounters(success=0, failure=0, running=1) assert state.done == False assert state.delays == [0.0] state = state.with_outcomes({'delete_fn': Outcome(final=True)}) assert state.extras == {reason: StateCounters(success=1, failure=1, running=1)} assert state.counts == StateCounters(success=1, failure=0, running=0) assert state.done == True assert state.delays == []
def test_purge_progress_cascades_to_subrefs(storage, handler): body = {'status': {'kopf': {'progress': { 'some-id': {'subrefs': ['sub1', 'sub2', 'sub3']}, 'sub1': {}, 'sub2': {}, # 'sub3' is intentionally absent -- should not be purged as already non-existent. 'sub-unrelated': {}, # should be ignored, as not related to the involved handlers. }}}} patch = Patch() state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state.purge(patch=patch, body=Body(body), storage=storage, handlers=[handler]) assert patch == {'status': {'kopf': {'progress': { 'some-id': None, 'sub1': None, 'sub2': None, }}}}
def test_repurposed_not_affecting_the_existing_handlers_from_storage(storage, handler, reason): body = {'status': {'kopf': {'progress': {'some-id': {'purpose': None}}}}} state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_handlers([handler]).with_purpose(reason).with_handlers([handler]) assert len(state) == 1 assert state.purpose == reason assert state['some-id'].purpose is None
def test_get_essence_removes_garbage_annotations_and_cleans_parents( annotation: str, cls: Type[DiffBaseStorage], ): body = Body({'metadata': {'annotations': {annotation: 'x'}}}) storage = cls() essence = storage.build(body=body) assert essence == {}
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_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
def test_get_essence_removes_system_fields_and_cleans_parents( field: str, cls: Type[DiffBaseStorage], ): body = Body({'metadata': {field: 'x'}}) storage = cls() essence = storage.build(body=body) assert essence == {}
def test_get_essence_removes_system_fields_but_keeps_extra_fields( field: str, cls: Type[DiffBaseStorage], ): body = Body({'metadata': {field: 'x', 'other': 'y'}}) storage = cls() essence = storage.build(body=body, extra_fields=['metadata.other']) assert essence == {'metadata': {'other': 'y'}}
def test_get_essence_removes_garbage_annotations_but_keeps_others( annotation: str, cls: Type[DiffBaseStorage], ): body = Body({'metadata': {'annotations': {annotation: 'x', 'other': 'y'}}}) storage = cls() essence = storage.build(body=body) assert essence == {'metadata': {'annotations': {'other': 'y'}}}