async def test_no_peering_tasks_with_no_peering_resources(settings, ensemble): settings.peering.mandatory = False insights = Insights() r1 = Resource(group='group1', version='version1', plural='plural1', namespaced=True) insights.resources.add(r1) insights.namespaces.add('ns1') await adjust_tasks( processor=processor, identity=Identity('...'), settings=settings, insights=insights, ensemble=ensemble, ) assert ensemble.watcher_tasks assert not ensemble.peering_tasks assert not ensemble.pinging_tasks assert not ensemble.freeze_toggles
async def test_followups_for_deletion_of_resource(registry, apis_mock, group1_empty_mock, timer, etype): e1 = RawEvent(type=etype, object=RawBody(spec={'group': 'group1'})) r1 = Resource(group='group1', version='version1', plural='plural1') insights = Insights() insights.resources.add(r1) async def delayed_injection(delay: float): await asyncio.sleep(delay) await process_discovered_resource_event(insights=insights, raw_event=e1, registry=registry) task = asyncio.create_task(delayed_injection(0.1)) async with timer, async_timeout.timeout(1.0): async with insights.revised: await insights.revised.wait() await task assert 0.1 < timer.seconds < 1.0 assert not insights.resources assert apis_mock.called assert group1_empty_mock.called
def test_on_create_minimal(cause_factory): registry = kopf.get_default_registry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, reason=Reason.CREATE) @kopf.on.create('group', 'version', 'plural') def fn(**_): pass handlers = registry.resource_changing_handlers.get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.CREATE assert handlers[0].field is None assert handlers[0].errors is None assert handlers[0].timeout is None assert handlers[0].retries is None assert handlers[0].backoff is None assert handlers[0].labels is None assert handlers[0].annotations is None assert handlers[0].when is None assert handlers[0].field is None assert handlers[0].old is None assert handlers[0].new is None
def test_on_create_minimal(cause_factory): registry = kopf.get_default_registry() resource = Resource('group', 'version', 'plural') cause = cause_factory(resource=resource, reason=Reason.CREATE) @kopf.on.create('group', 'version', 'plural') def fn(**_): pass with pytest.deprecated_call(match=r"cease using the internal registries"): handlers = registry.get_resource_changing_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.CREATE assert handlers[0].field is None assert handlers[0].errors is None assert handlers[0].timeout is None assert handlers[0].retries is None assert handlers[0].backoff is None assert handlers[0].labels is None assert handlers[0].annotations is None assert handlers[0].when is None
def test_on_field_with_cooldown(mocker, cause_factory): registry = kopf.get_default_registry() resource = Resource('group', 'version', 'plural') diff = [('op', ('field', 'subfield'), 'old', 'new')] cause = cause_factory(resource=resource, reason=Reason.UPDATE, diff=diff) mocker.patch('kopf.reactor.registries.match', return_value=True) with pytest.deprecated_call(match=r"use backoff="): @kopf.on.field('group', 'version', 'plural', field='field.subfield', cooldown=78) def fn(**_): pass handlers = registry.resource_changing_handlers.get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 with pytest.deprecated_call(match=r"use handler.backoff"): assert handlers[0].cooldown == 78
def test_url_for_a_list_of_namespaced_custom_resources_clusterwide(): resource = Resource('group', 'version', 'plural', namespaced=True) url = resource.get_url(namespace=None) assert url == '/apis/group/version/plural'
def test_name_of_corev1_resource(): resource = Resource('', 'v1', 'plural') name = resource.name assert name == 'plural'
def resource(): """ The resource used in the tests. Usually mocked, so it does not matter. """ return Resource('zalando.org', 'v1', 'kopfexamples')
def namespaced_peering_resource(request): return Resource(*request.param[:3], namespaced=True)
def cluster_resource(): """ The resource used in the tests. Usually mocked, so it does not matter. """ return Resource('kopf.dev', 'v1', 'kopfexamples', namespaced=False)
def cluster_peering_resource(request): return Resource(*request.param[:3], namespaced=False)
NAMESPACES, Backbone, Resource, Selector @pytest.mark.parametrize('selector', [ CRDS, EVENTS, NAMESPACES, CLUSTER_PEERINGS, NAMESPACED_PEERINGS, ]) def test_empty_backbone(selector: Selector): backbone = Backbone() assert len(backbone) == 0 assert set(backbone) == set() with pytest.raises(KeyError): assert backbone[selector] @pytest.mark.parametrize('selector, resource', [ (CRDS, Resource('apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions')), (CRDS, Resource('apiextensions.k8s.io', 'v1', 'customresourcedefinitions')), (CRDS, Resource('apiextensions.k8s.io', 'vX', 'customresourcedefinitions')), (EVENTS, Resource('', 'v1', 'events')), (NAMESPACES, Resource('', 'v1', 'namespaces')), (CLUSTER_PEERINGS, Resource('kopf.dev', 'v1', 'clusterkopfpeerings')), (NAMESPACED_PEERINGS, Resource('kopf.dev', 'v1', 'kopfpeerings')), (CLUSTER_PEERINGS, Resource('zalando.org', 'v1', 'clusterkopfpeerings')), (NAMESPACED_PEERINGS, Resource('zalando.org', 'v1', 'kopfpeerings')), ]) async def test_refill_populates_the_resources(selector: Selector, resource: Resource): backbone = Backbone() await backbone.fill(resources=[resource]) assert len(backbone) == 1 assert set(backbone) == {selector} assert backbone[selector] == resource
def test_url_for_a_specific_clusterscoped_corev1_subresource_clusterwide(): resource = Resource('', 'v1', 'plural', namespaced=False) url = resource.get_url(namespace=None, name='name-a.b', subresource='status') assert url == '/api/v1/plural/name-a.b/status'
def test_url_for_a_specific_namespaced_custom_subresource_in_a_namespace(): resource = Resource('group', 'version', 'plural', namespaced=True) url = resource.get_url(namespace='ns-a.b', name='name-a.b', subresource='status') assert url == '/apis/group/version/namespaces/ns-a.b/plural/name-a.b/status'
def test_url_with_arbitrary_params(): resource = Resource('group', 'version', 'plural') url = resource.get_url( params=dict(watch='true', resourceVersion='abc%def xyz')) assert url == '/apis/group/version/plural?watch=true&resourceVersion=abc%25def+xyz'
def test_url_for_a_list_of_namespaced_corev1_resources_in_a_namespace(): resource = Resource('', 'v1', 'plural', namespaced=True) url = resource.get_url(namespace='ns-a.b') assert url == '/api/v1/namespaces/ns-a.b/plural'
def test_url_for_a_list_of_namespaced_corev1_resources_clusterwide(): resource = Resource('', 'v1', 'plural', namespaced=True) url = resource.get_url(namespace=None) assert url == '/api/v1/plural'
def test_different_resource(): selector = Selector('group1', 'version1', 'plural1') resource = Resource('group2', 'version2', 'plural2') handler = Mock(selector=selector) matches = _matches_resource(handler, resource) assert not matches
def test_url_for_a_list_of_namespaced_custom_resources_in_a_namespace(): resource = Resource('group', 'version', 'plural', namespaced=True) url = resource.get_url(namespace='ns-a.b') assert url == '/apis/group/version/namespaces/ns-a.b/plural'
def test_url_for_a_specific_namespaced_corev1_subresource_in_a_namespace(): resource = Resource('', 'v1', 'plural', namespaced=True) url = resource.get_url(namespace='ns-a.b', name='name-a.b', subresource='status') assert url == '/api/v1/namespaces/ns-a.b/plural/name-a.b/status'
def test_url_for_a_specific_clusterscoped_custom_resource_clusterwide(): resource = Resource('group', 'version', 'plural', namespaced=False) url = resource.get_url(namespace=None, name='name-a.b') assert url == '/apis/group/version/plural/name-a.b'
def test_api_version_of_custom_resource(): resource = Resource('group', 'version', 'plural') api_version = resource.api_version assert api_version == 'group/version'
async def test_refill_is_cumulative_ie_does_not_reset(): backbone = Backbone() await backbone.fill(resources=[Resource('', 'v1', 'namespaces')]) await backbone.fill(resources=[Resource('', 'v1', 'events')]) assert len(backbone) == 2 assert set(backbone) == {NAMESPACES, EVENTS}
def test_catchall_with_none(): resource = Resource('group2', 'version2', 'plural2') handler = Mock(selector=None) matches = _matches_resource(handler, resource) assert matches
def peering_resource(request): return Resource(*request.param[:3], namespaced=request.param[3])
def test_name_of_custom_resource(): resource = Resource('group', 'version', 'plural') name = resource.name assert name == 'plural.group'
def resource(request): """ The resource used in the tests. Usually mocked, so it does not matter. """ return Resource('kopf.dev', 'v1', 'kopfexamples', namespaced=request.param)
def test_creation_with_no_args(): with pytest.raises(TypeError): Resource()
def test_api_version_of_corev1_resource(): resource = Resource('', 'v1', 'plural') api_version = resource.api_version assert api_version == 'v1'
async def patch_obj( *, resource: references.Resource, namespace: references.Namespace, name: Optional[str], patch: patches.Patch, context: Optional[auth.APIContext] = None, # injected by the decorator ) -> Optional[bodies.RawBody]: """ Patch a resource of specific kind. Unlike the object listing, the namespaced call is always used for the namespaced resources, even if the operator serves the whole cluster (i.e. is not namespace-restricted). Returns the patched body. The patched body can be partial (status-only, no-status, or empty) -- depending on whether there were fields in the body or in the status to patch; if neither had fields for patching, the result is an empty body. The result should only be used to check against the patch: if there was nothing to patch, it does not matter if the fields are absent. Returns ``None`` if the underlying object is absent, as detected by trying to patch it and failing with HTTP 404. This can happen if the object was deleted in the operator's handlers or externally during the processing, so that the framework was unaware of these changes until the last moment. """ if context is None: raise RuntimeError("API instance is not injected by the decorator.") as_subresource = 'status' in resource.subresources body_patch = dict( patch) # shallow: for mutation of the top-level keys below. status_patch = body_patch.pop('status', None) if as_subresource else None # Patch & reconstruct the actual body as reported by the server. The reconstructed body can be # partial or empty -- if the body/status patches are empty. This is fine: it is only used # to verify that the patched fields are matching the patch. No patch? No mismatch! try: patched_body = bodies.RawBody() if body_patch: response = await context.session.patch( url=resource.get_url(server=context.server, namespace=namespace, name=name), headers={'Content-Type': 'application/merge-patch+json'}, json=body_patch, ) patched_body = await errors.parse_response(response) if status_patch: response = await context.session.patch( url=resource.get_url( server=context.server, namespace=namespace, name=name, subresource='status' if as_subresource else None), headers={'Content-Type': 'application/merge-patch+json'}, json={'status': status_patch}, ) patched_body['status'] = ( await errors.parse_response(response)).get('status') return patched_body except errors.APINotFoundError: return None