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}")
Beispiel #2
0
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)
Beispiel #3
0
def test_removal_of_the_key():
    patch = Patch()
    patch['xyz'] = None
    jsonpatch = patch.as_json_patch()
    assert jsonpatch == [
        {
            'op': 'remove',
            'path': '/xyz'
        },
    ]
Beispiel #4
0
def test_removal_of_the_subkey():
    patch = Patch()
    patch['xyz'] = {'abc': None}
    jsonpatch = patch.as_json_patch()
    assert jsonpatch == [
        {
            'op': 'remove',
            'path': '/xyz/abc'
        },
    ]
Beispiel #5
0
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)
Beispiel #6
0
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)
Beispiel #7
0
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
Beispiel #12
0
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
Beispiel #13
0
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:",
                ])
Beispiel #14
0
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"
    ])
Beispiel #15
0
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
Beispiel #16
0
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
Beispiel #19
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_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
Beispiel #22
0
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'
Beispiel #24
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
Beispiel #25
0
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
Beispiel #26
0
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
Beispiel #28
0
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
Beispiel #29
0
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