Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ exit code and output are available to the test (for additional assertions).
.. note::
The operator runs against the cluster which is currently authenticated ---
same as if would be executed with `kopf run`.


Mock server
===========

KMock is a supplimentary project to run a local mock server for any HTTP API, and for Kubernetes API in particular — with extended supported of Kubernetes API endpoints, resource discovery, and implicit in-memory object persistence.

Use KMock when you need to run a very lightweight simulation of the Kubernetes API without deploying the heavy Kubernetes cluster nearby, for example when migrating to/from Kopf.


.. code-block:: python

import kmock
import requests

def test_object_patching(kmock: kmock.KubernetesEmulator) -> None:
kmock.objects['kopf.dev/v1/kopfexamples', 'ns1', 'name1'] = {'spec': 123}
requests.patch(str(kmock.url) + '/kopf.dev/v1/namespaces/ns1/name1', json={'spec': 456})
assert len(kmock.requests) == 1
assert kmock.requests[0].method == 'patch'
assert kmock.objects['kopf.dev/v1/kopfexamples', 'ns1', 'name1'] == {'spec': 456}

KMock's detailed documentation is out of scope of Kopf's documentation. The project and its documentation can be found at:

* https://kmock.readthedocs.io/
* https://github.com/nolar/kmock
* https://pypi.org/project/kmock/
7 changes: 7 additions & 0 deletions kopf/_cogs/aiokits/aiotasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,13 @@ async def spawn(
await self._pending_coros.put(SchedulerJob(coro=coro, name=name))
self._condition.notify_all() # -> task_spawner()

# Give the spawner some asyncio cycles to actually spawn and maybe end the task instantly.
# This barely ever happens with real worker(); it is mainly for tests in `test_queueing.py`:
# Depending on luck, they were arriving to `_wait_for_depletion()` with their mocked workers
# either "done", or "pending", thus giving looptime==0 or looptime==exit_timeout (randomly).
# With this extra sleep, such mocked workers are now "done" and the looptime==0.
await asyncio.sleep(0)

def _can_spawn(self) -> bool:
return (not self._pending_coros.empty() and
(self._limit is None or len(self._running_tasks) < self._limit))
Expand Down
2 changes: 1 addition & 1 deletion kopf/_kits/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async def _serve_fn(request: aiohttp.web.Request) -> aiohttp.web.Response:
# multi-threaded sockets are not really used -- high load is not expected for webhooks.
addr = self.addr or None # None is aiohttp's "any interface"
port = self.port or self._allocate_free_port()
site = aiohttp.web.TCPSite(runner, addr, port, ssl_context=context, reuse_port=True)
site = aiohttp.web.TCPSite(runner, addr, port, ssl_context=context, reuse_port=True, reuse_address=True)
await site.start()

# Log with the actual URL: normalised, with hostname/port set.
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ docs = [
"furo",
]
test = [
"aresponses",
"astpath[xpath]",
"certbuilder",
"certvalidator",
"codecov",
"coverage>=7.12.0",
"freezegun",
"kmock>=0.3",
"looptime>=0.7",
"lxml",
"pyngrok",
Expand Down Expand Up @@ -105,6 +105,11 @@ lint = [
"isort",
"pre-commit",
]
dev = [
{include-group = "docs"},
{include-group = "lint"},
{include-group = "test"},
]

[tool.setuptools_scm]
version_file = "kopf/_cogs/helpers/versions.py"
Expand Down
35 changes: 15 additions & 20 deletions tests/admission/test_webhook_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# so we mock all external(!) libraries to return the results as we expect them.
# This reduces the quality of the tests, but makes them simple.
@pytest.fixture(autouse=True)
def pathmock(mocker, fake_vault, enforced_context, aresponses, hostname):
def pathmock(mocker, fake_vault):
mocker.patch('ssl.get_server_certificate', return_value='')
mocker.patch('certvalidator.ValidationContext')
validator = mocker.patch('certvalidator.CertificateValidator')
Expand All @@ -17,14 +17,14 @@
return pathmock


async def test_no_detection(hostname, aresponses):
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.2.3'})
async def test_no_detection(kmock):
kmock['get /version'] << {'gitVersion': 'v1.2.3'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
hostname = await ClusterDetector().guess_host()
assert hostname is None


async def test_dependencies(hostname, aresponses, no_certvalidator):
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.2.3'})
async def test_dependencies(kmock, no_certvalidator):
kmock['get /version'] << {'gitVersion': 'v1.2.3'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
with pytest.raises(ImportError) as err:
await ClusterDetector().guess_host()
assert "pip install certvalidator" in str(err.value)
Expand Down Expand Up @@ -60,46 +60,41 @@
assert hostname == 'host.k3d.internal'


async def test_k3d_via_version_infix(hostname, aresponses):
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.20.4+k3s1'})
async def test_k3d_via_version_infix(kmock):
kmock['get /version'] << {'gitVersion': 'v1.20.4+k3s1'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
hostname = await ClusterDetector().guess_host()
assert hostname == 'host.k3d.internal'


async def test_server_detects(responder, aresponses, hostname, caplog, assert_logs):
caplog.set_level(0)
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.20.4+k3s1'})
async def test_server_detects(kmock, responder, assert_logs):
kmock['get /version'] << {'gitVersion': 'v1.20.4+k3s1'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
server = WebhookAutoServer(insecure=True)
async with server:
async for _ in server(responder.fn):
break # do not sleep
assert_logs(["Cluster detection found the hostname: host.k3d.internal"])


async def test_server_works(
responder, aresponses, hostname, caplog, assert_logs):
caplog.set_level(0)
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.20.4'})
async def test_server_works(kmock, responder, assert_logs):
kmock['get /version'] << {'gitVersion': 'v1.20.4'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
server = WebhookAutoServer(insecure=True)
async with server:
async for _ in server(responder.fn):
break # do not sleep
assert_logs(["Cluster detection failed, running a simple local server"])


async def test_tunnel_detects(responder, pyngrok_mock, aresponses, hostname, caplog, assert_logs):
caplog.set_level(0)
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.20.4+k3s1'})
async def test_tunnel_detects(kmock, responder, pyngrok_mock, assert_logs):
kmock['get /version'] << {'gitVersion': 'v1.20.4+k3s1'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
server = WebhookAutoTunnel()
async with server:
async for _ in server(responder.fn):
break # do not sleep
assert_logs(["Cluster detection found the hostname: host.k3d.internal"])


async def test_tunnel_works(responder, pyngrok_mock, aresponses, hostname, caplog, assert_logs):
caplog.set_level(0)
aresponses.add(hostname, '/version', 'get', {'gitVersion': 'v1.20.4'})
async def test_tunnel_works(kmock, responder, pyngrok_mock, assert_logs):
kmock['get /version'] << {'gitVersion': 'v1.20.4'}

Check notice

Code scanning / CodeQL

Statement has no effect Note test

This statement has no effect.
server = WebhookAutoTunnel()
async with server:
async for _ in server(responder.fn):
Expand Down
Loading
Loading