def test_fetching_from_annotations_storage(cls): storage = cls(prefix='my-operator.example.com', verbose=True) body = Body({ 'metadata': { 'annotations': { 'my-operator.example.com/id1': CONTENT_JSON_1, } } }) content = storage.fetch(body=body, key=HandlerId('id1')) assert content == CONTENT_DATA_1
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
def test_created_empty_from_empty_storage_with_handlers( storage, handler, body): state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) assert len(state) == 0 assert state.purpose is None assert state.done == True assert state.delay is None assert state.delays == [] assert state.counts == StateCounters(success=0, failure=0, running=0) assert state.extras == {}
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': HandlerOutcome(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_when_already_empty_in_body_but_not_in_patch( storage, handler): body = {} patch = Patch( {'status': { 'kopf': { 'progress': { 'some-id': { 'retries': 5 } } } }}) state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state.purge(patch=patch, body=Body(body), storage=storage, handlers=[handler]) assert not patch
def test_daemon_kwargs(resource, indices): 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=indices, resource=resource, patch=Patch(), memo=Memo(), body=Body(body), stopper=DaemonStopper(), ) kwargs = build_kwargs(cause=cause, extrakwarg=123) assert set(kwargs) == { 'extrakwarg', 'logger', 'index1', 'index2', 'resource', 'patch', 'memo', 'body', 'spec', 'status', 'meta', 'uid', 'name', 'namespace', 'labels', 'annotations' } assert kwargs['extrakwarg'] == 123 assert kwargs['resource'] is cause.resource assert kwargs['index1'] is indices['index1'] assert kwargs['index2'] is indices['index2'] 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 'stopped' not in kwargs
def test_get_essence_removes_status_but_keeps_extra_fields( cls: Type[DiffBaseStorage], ): body = Body( {'status': { 'kopf': { 'progress': 'x', 'anything': 'y' }, 'other': 'z' }}) storage = cls() essence = storage.build(body=body, extra_fields=['status.other']) assert essence == {'status': {'other': 'z'}}
def test_get_essence_removes_status_and_cleans_parents( cls: Type[DiffBaseStorage], ): body = Body( {'status': { 'kopf': { 'progress': 'x', 'anything': 'y' }, 'other': 'z' }}) storage = cls() essence = storage.build(body=body) assert essence == {}
def remove_finalizers( *, body: bodies.Body, patch: patches.Patch, ) -> None: if has_finalizers(body=body): 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)
def test_with_handlers_relevant_to_the_purpose(storage, handler, body, expected_counts, expected_done, expected_delays, reason): body['status']['kopf']['progress']['some-id']['purpose'] = reason.value state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_purpose(reason) assert len(state) == 1 assert state.extras == {} assert state.counts == expected_counts assert state.done == expected_done assert state.delays == expected_delays
def test_field_longer_than_diff_for_wrong_field(cause_with_diff, registry, resource, reason, decorator): @decorator(*resource, field='level1.level2.level3') def some_fn(**_): ... cause = cause_with_diff cause.reason = reason cause.old = {'level1': {'level2': 'old'}} cause.new = {'level1': {'level2': 'new'}} cause.body = Body({'level1': {'level2': 'new'}}) handlers = registry._resource_changing.get_handlers(cause) assert not handlers
def test_storing_to_annotations_storage_populates_keys(cls): storage = cls(prefix='my-operator.example.com', key='diff-base') patch = Patch() body = Body({}) storage.store(body=body, patch=patch, essence=ESSENCE_DATA_1) assert patch assert patch.meta.annotations['my-operator.example.com/diff-base'][ 0] != '\n' assert patch.meta.annotations['my-operator.example.com/diff-base'][ -1] == '\n' assert patch.meta.annotations['my-operator.example.com/diff-base'].strip( ) == ESSENCE_JSON_1
def test_for_no_op(kwargs, event, finalizers, deletion_ts, old, annotations, content, requires_finalizer): event = {'type': event, 'object': {'metadata': {}}} event['object'].update(content) event['object']['metadata'].update(finalizers) event['object']['metadata'].update(deletion_ts) event['object']['metadata'].update(annotations) cause = detect_resource_changing_cause(raw_event=event, body=Body(event['object']), old=old, **kwargs) assert cause.reason == Reason.NOOP check_kwargs(cause, kwargs)
def allow_deletion( *, body: bodies.Body, patch: patches.Patch, ) -> None: if is_deletion_blocked(body=body): 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)
def test_fetching_from_annotations_storage(cls, prefix, suffix): storage = cls(prefix='my-operator.example.com', key='diff-base') body = Body({ 'metadata': { 'annotations': { 'my-operator.example.com/diff-base': prefix + ESSENCE_JSON_1 + suffix, } } }) content = storage.fetch(body=body) assert content == ESSENCE_DATA_1
def test_removal_distinguishes_by_uid(): owner1 = copy.deepcopy(OWNER) owner2 = copy.deepcopy(OWNER) owner3 = copy.deepcopy(OWNER) owner1['metadata']['uid'] = 'uid-a' owner2['metadata']['uid'] = 'uid-b' owner3['metadata']['uid'] = 'uid-c' obj = {} # Three very similar owners added, different only by uid. # One is removed, others must stay (even if kinds/names are the same). 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 uids = {ref['uid'] for ref in obj['metadata']['ownerReferences']} assert uids == {'uid-b', 'uid-c'}
def test_fetching_from_status_storage(cls): storage = cls(field='status.my-operator') body = Body({ 'status': { 'my-operator': { 'id1': CONTENT_DATA_1, 'id2': CONTENT_DATA_2 } } }) content = storage.fetch(body=body, key=HandlerId('id1')) assert content == CONTENT_DATA_1
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 = ResourceChangingCause( logger=logging.getLogger('kopf.test.fake.logger'), 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, cause=cause) assert fn.called assert fn.call_count == 1 assert len(fn.call_args[1]) >= 2 assert fn.call_args[1]['cause'] is cause assert fn.call_args[1]['event'] is cause.reason # deprecated assert fn.call_args[1]['reason'] is cause.reason assert fn.call_args[1]['body'] is cause.body assert fn.call_args[1]['spec'] == cause.body['spec'] assert fn.call_args[1]['meta'] == cause.body['metadata'] assert fn.call_args[1]['status'] == cause.body['status'] assert fn.call_args[1]['diff'] is cause.diff assert fn.call_args[1]['old'] is cause.old assert fn.call_args[1]['new'] is cause.new assert fn.call_args[1]['patch'] is cause.patch assert fn.call_args[1]['logger'] is cause.logger assert fn.call_args[1]['uid'] == cause.body['metadata']['uid'] assert fn.call_args[1]['name'] == cause.body['metadata']['name'] assert fn.call_args[1]['namespace'] == cause.body['metadata']['namespace']
def purge(self, patch: patches.Patch, body: bodies.Body) -> None: if 'progress' in body.get('status', {}).get('kopf', {}): patch_storage = patch.setdefault('status', {}).setdefault('kopf', {}) patch_storage['progress'] = None elif 'progress' in patch.get('status', {}).get('kopf', {}): del patch['status']['kopf']['progress'] # Avoid storing the empty status dicts (but do so if they have any content). if 'status' in patch and 'kopf' in patch[ 'status'] and not patch['status']['kopf']: del patch['status']['kopf'] if 'status' in patch and not patch['status']: del patch['status']
def test_storing_to_annotations_storage_overwrites_old_content(cls): storage = cls(name='my-operator.example.com/diff-base') patch = Patch() body = Body({}) storage.store(body=body, patch=patch, essence=ESSENCE_DATA_1) storage.store(body=body, patch=patch, essence=ESSENCE_DATA_2) assert patch assert patch.meta.annotations['my-operator.example.com/diff-base'][ 0] != '\n' assert patch.meta.annotations['my-operator.example.com/diff-base'][ -1] == '\n' assert patch.meta.annotations['my-operator.example.com/diff-base'].strip( ) == ESSENCE_JSON_2
def test_appending_to_dict(): obj = {} kopf.append_owner_reference(obj, owner=Body(OWNER)) assert 'metadata' in obj assert 'ownerReferences' in obj['metadata'] assert isinstance(obj['metadata']['ownerReferences'], list) assert len(obj['metadata']['ownerReferences']) == 1 assert isinstance(obj['metadata']['ownerReferences'][0], dict) assert obj['metadata']['ownerReferences'][0]['apiVersion'] == OWNER_API_VERSION assert obj['metadata']['ownerReferences'][0]['kind'] == OWNER_KIND assert obj['metadata']['ownerReferences'][0]['name'] == OWNER_NAME assert obj['metadata']['ownerReferences'][0]['uid'] == OWNER_UID
def _build_key( self, body: bodies.Body, ) -> str: """ Construct an immutable persistent key of a resource. Generally, a uid is sufficient, as it is unique within the cluster. But it can be e.g. plural/namespace/name triplet, or anything else, even of different types (as long as it satisfies the type checkers). But it must be consistent within a single process lifetime. """ return body.get('metadata', {}).get('uid') or ''
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 from_body( cls, body: bodies.Body, *, handlers: Collection[handlers_.BaseHandler], ) -> "State": storage = body.get('status', {}).get('kopf', {}) progress = storage.get('progress', {}) content = {} content.update({ handler.id: (HandlerState.from_scratch() if handler.id not in progress else HandlerState.from_dict(progress[handler.id])) for handler in handlers }) return cls(content)
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_with_handlers_irrelevant_to_the_purpose(storage, handler, body, expected_extras, stored_reason, processed_reason): body['status']['kopf']['progress']['some-id'][ 'purpose'] = stored_reason.value state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_purpose(processed_reason) assert len(state) == 1 assert state.extras[stored_reason] == expected_extras assert state.counts == StateCounters(success=0, failure=0, running=0) assert state.done == True assert state.delays == []
def test_catchall_handlers_with_when_not_match(registry, register_fn, resource, when): cause = ResourceChangingCause( resource=resource, reason='some-reason', diff=None, body=Body({'spec': {'name': 'test'}}), logger=None, patch=None, memo=None, initial=None ) register_fn(some_fn, reason=None, field=None, when=when) with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): handlers = registry.get_resource_changing_handlers(cause) assert not handlers
def test_storing_to_status_storage_overwrites_old_content(cls): storage = cls(field='status.my-operator') patch = Patch() body = Body({}) storage.store(body=body, patch=patch, key=HandlerId('id1'), record=CONTENT_DATA_1) storage.store(body=body, patch=patch, key=HandlerId('id1'), record=CONTENT_DATA_2) assert patch assert patch['status']['my-operator']['id1'] == CONTENT_DATA_2
def test_purging_of_annotations_storage_nullifies_content(cls): storage = cls(prefix='my-operator.example.com', verbose=True) patch = Patch() body = Body({ 'metadata': { 'annotations': { 'my-operator.example.com/id1': CONTENT_JSON_1, } } }) storage.purge(body=body, patch=patch, key=HandlerId('id1')) assert patch assert patch['metadata']['annotations'][ 'my-operator.example.com/id1'] is None
def test_touching_via_annotations_storage_with_none_when_present(cls): storage = cls(prefix='my-operator.example.com', touch_key='my-dummy') patch = Patch() body = Body({ 'metadata': { 'annotations': { 'my-operator.example.com/my-dummy': 'something' } } }) storage.touch(body=body, patch=patch, value=None) assert patch assert patch['metadata']['annotations'][ 'my-operator.example.com/my-dummy'] is None