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_purge_progress_when_already_empty_in_body_but_not_in__patch( storage, handler): body = {} patch = Patch( {'status': { 'kopf': { 'progress': { 'some-id': { 'retries': 5 } } } }}) origbody = copy.deepcopy(body) state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state.purge(patch=patch, body=Body(body), storage=storage) assert not patch assert body == origbody # not modified
def test_for_delete( kwargs, event, finalizers, deletion_ts, requires_finalizer): event = {'type': event, 'object': {'metadata': {}}} event['object']['metadata'].update(finalizers) event['object']['metadata'].update(deletion_ts) cause = detect_resource_changing_cause( raw_event=event, body=Body(event['object']), **kwargs) assert cause.reason == Reason.DELETE check_kwargs(cause, kwargs)
def test_store_failure(storage, handler, expected_retries, expected_stopped, body): error = Exception('some-error') origbody = copy.deepcopy(body) patch = Patch() state = State.from_storage(body=Body(body), handlers=[handler], storage=storage) state = state.with_outcomes( outcomes={handler.id: HandlerOutcome(final=True, exception=error)}) state.store(patch=patch, body=Body(body), storage=storage) assert patch['status']['kopf']['progress']['some-id']['success'] is False assert patch['status']['kopf']['progress']['some-id']['failure'] is True 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'] == 'some-error' assert body == origbody # not modified
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 is reason assert state['some-id'].purpose is None
def test_fetching_from_annotations_storage(cls): storage = cls(name='my-operator.example.com/diff-base') body = Body({ 'metadata': { 'annotations': { 'my-operator.example.com/diff-base': ESSENCE_JSON_1, } } }) content = storage.fetch(body=body) assert content == ESSENCE_DATA_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_storing_to_status_storage_overwrites_old_content( cls: Type[DiffBaseStorage]): storage = cls(field='status.my-operator.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.status['my-operator']['diff-base'][0] != '\n' assert patch.status['my-operator']['diff-base'][-1] != '\n' assert patch.status['my-operator']['diff-base'] == ESSENCE_JSON_2
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_field_longer_than_diff_for_right_field(cause_with_diff, registry, resource, old, new, reason, decorator): @decorator(*resource, field='level1.level2.level3') def some_fn(**_): ... cause = cause_with_diff cause.reason = reason cause.old = {'level1': {'level2': old}} if old is not None else {'level1': {'level2': {}}} cause.new = {'level1': {'level2': new}} if new is not None else {'level1': {'level2': {}}} cause.body = Body(cause.new) handlers = registry._resource_changing.get_handlers(cause) assert handlers
def test_daemon_async_stopper(resource, indices): cause = DaemonCause( logger=logging.getLogger('kopf.test.fake.logger'), indices=indices, resource=resource, patch=Patch(), memo=Memo(), body=Body({}), stopper=DaemonStopper(), ) kwargs = build_kwargs(cause=cause, _sync=False) assert kwargs['stopped'] is cause.stopper.async_checker
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_fetching_from_status_storage(cls, prefix, suffix): storage = cls(field='status.my-operator.diff-base') body = Body({ 'status': { 'my-operator': { 'diff-base': prefix + ESSENCE_JSON_1 + suffix } } }) content = storage.fetch(body=body) assert content == ESSENCE_DATA_1
def test_appending_to_kubernetes_model(kubernetes_model): kubernetes_model.metadata = None kopf.append_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) == 1 assert kubernetes_model.metadata.owner_references[ 0].api_version == OWNER_API_VERSION assert kubernetes_model.metadata.owner_references[0].kind == OWNER_KIND assert kubernetes_model.metadata.owner_references[0].name == OWNER_NAME assert kubernetes_model.metadata.owner_references[0].uid == OWNER_UID
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_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_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
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_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_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 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 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_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_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
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 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_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_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 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