Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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 == {}
Exemple #4
0
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 == []
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
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'}}
Exemple #8
0
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 == {}
Exemple #9
0
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)
Exemple #10
0
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
Exemple #11
0
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)
Exemple #14
0
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
Exemple #18
0
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']
Exemple #19
0
    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
Exemple #21
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
Exemple #22
0
    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 ''
Exemple #23
0
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
Exemple #24
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)
Exemple #25
0
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,
                }
            }
        }
    }
Exemple #26
0
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 == []
Exemple #27
0
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