Skip to content

Commit

Permalink
WIP: admission webhooks
Browse files Browse the repository at this point in the history
Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
  • Loading branch information
nolar committed Mar 24, 2021
1 parent e90ceaa commit 0dc46bf
Show file tree
Hide file tree
Showing 50 changed files with 4,032 additions and 28 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ the same packages, the same developer(s).
* Dynamically generated or conditional sub-handlers (an advanced feature).
* Timers that tick as long as the resource exists, optionally with a delay since the last change.
* Daemons that run as long as the resource exists (in threads or asyncio-tasks).
* Validating and mutating admission webhook (with dev-mode tunneling).
* Live in-memory indexing of resources or their excerpts.
* Filtering with stealth mode (no logging): by arbitrary filtering functions,
by labels/annotations with values, presence/absence, or dynamic callbacks.
* In-memory all-purpose containers to store non-serializable objects for individual resources.
Expand Down
662 changes: 662 additions & 0 deletions docs/admission.rst

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/deployment-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ rules:
resources: [namespaces]
verbs: [list, watch]

# Framework: admission webhook configuration management.
- apiGroups: [admissionregistration.k8s.io/v1, admissionregistration.k8s.io/v1beta1]
resources: [validatingwebhookconfigurations, mutatingwebhookconfigurations]
verbs: [create, patch]

# Application: read-only access for watching cluster-wide.
- apiGroups: [kopf.dev]
resources: [kopfexamples]
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Kopf: Kubernetes Operators Framework
scopes
memos
indexing
admission

.. toctree::
:maxdepth: 2
Expand Down
72 changes: 72 additions & 0 deletions docs/kwargs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,75 @@ Its ``.wait()`` method can be used to replace ``time.sleep()``
or ``asyncio.sleep()`` for faster (instant) termination on resource deletion.

See more: :doc:`daemons`.


Resource admission kwargs
=========================

.. kwarg:: dryrun

Dry run
-------

Admission handlers, both validating and mutating, must skip any side effects
if ``dryrun`` is ``True``. It is ``True`` when a dry-run API request is made,
e.g. with ``kubectl --dry-run=server ...``.

Regardless of ``dryrun`, the handlers must not make any side effects
unless they declare themselves as ``side_effects=True``.

See more: :doc:`admission`.


.. kwarg:: warnings

Admission warnings
------------------

``warnings`` (``list[str]``) is a **mutable** list of string used as warnings.
The admission webhook handlers can populate the list with warnings (strings),
and the webhook servers/tunnels return them to Kubernetes, which shows them
to ``kubectl``.

See more: :doc:`admission`.


.. kwarg:: userinfo

User information
----------------

``userinfo`` (``Mapping[str, Any]``) is an information about a user that
sends the API request to Kubernetes.

It usually contains the keys ``'username'``, ``'uid'``, ``'groups'``,
but this might change in the future. The information is provided exactly
as Kubernetes sends it in the admission request.

See more: :doc:`admission`.


.. kwarg:: headers
.. kwarg:: sslpeer

Request credentials
-------------------

For rudimentary authentication and authorization, Kopf passes the information
from the admission requests to the admission handlers as is,
without additional interpretation of it.

``headers`` (``Mapping[str, str]``) contains all HTTPS request headers,
including ``Authorization: Basic ...``, ``Authorization: Bearer ...``.

``sslpeer`` (``Mapping[str, Any]``) contains the SSL peer information
as returned by `ssl.SSLSocket.getpeercert`. It is ``None`` if no proper
SSL client certificate was provided (i.e. by apiservers talking to webhooks),
or if the SSL protocol could not verify the provided certificate with its CA.

.. note::
This is an identity of the apiservers that send the admission request,
not of the user or an app that sends the API request to Kubernetes.
For the user's identity, use :kwarg:`userinfo`.

See more: :doc:`admission`.
57 changes: 57 additions & 0 deletions examples/17-admission/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pathlib

import kopf


@kopf.on.startup()
def config(settings: kopf.OperatorSettings, **_):
ROOT = (pathlib.Path.cwd() / pathlib.Path(__file__)).parent.parent.parent
settings.admission.managed = 'auto.kopf.dev'
# settings.admission.server = kopf.WebhookServer()
# settings.admission.server = kopf.WebhookServer(certfile=ROOT/'cert.pem', pkeyfile=ROOT/'key.pem', port=1234)
# settings.admission.server = kopf.WebhookK3dServer(certfile=ROOT/'k3d-cert.pem', pkeyfile=ROOT/'k3d-key.pem', port=1234)
# settings.admission.server = kopf.WebhookK3dServer(port=1234, cadump=ROOT/'ca.pem', verify_cafile=ROOT/'client-cert.pem')
settings.admission.server = kopf.WebhookK3dServer(cadump=ROOT/'ca.pem')
# settings.admission.server = kopf.WebhookNgrokTunnel()
# settings.admission.server = kopf.WebhookNgrokTunnel(binary="/usr/local/bin/ngrok", token='...', port=1234)
# settings.admission.server = kopf.WebhookNgrokTunnel(binary="/usr/local/bin/ngrok", port=1234, path='/xyz', region='eu')
# settings.admission.server = kopf.WebhookInletsTunnel(...)


@kopf.on.validation('kex', field='spec.field2', value='value')
def validate1(spec, dryrun, **_):
print(f'{dryrun=}')
if spec.get('field') == 'value2':
raise kopf.AdmissionError("Meh! I don't like it. Change the field.")


@kopf.on.validation('kex')
def authhook(headers, sslpeer, warnings, **_):
print(f'{headers=}')
print(f'{sslpeer=}')
if not sslpeer:
warnings.append("SSL peer is not identified.")
else:
common_name = None
for key, val in sslpeer['subject'][0]:
if key == 'commonName':
common_name = val
break
else:
warnings.append("SSL peer's common name is absent.")
if common_name is not None:
warnings.append(f"SSL peer is {common_name}.")


# @kopf.on.validation('kex')
# def validate2(**_):
# raise kopf.AdmissionError("I'm too lazy anyway. Go away!", status=555)


@kopf.on.mutation('kex')
def mutate1(patch: kopf.Patch, **_):
patch.spec['injected'] = 123


# Marks for the e2e tests (see tests/e2e/test_examples.py):
E2E_SUCCESS_COUNTS = {} # we do not care: pods can have 6-10 updates here.
30 changes: 30 additions & 0 deletions kopf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
from kopf.reactor import (
lifecycles, # as a separate name on the public namespace
)
from kopf.reactor.admission import (
AdmissionError,
)
from kopf.reactor.handling import (
TemporaryError,
PermanentError,
Expand Down Expand Up @@ -130,6 +133,14 @@
Resource,
EVERYTHING,
)
from kopf.structs.reviews import (
WebhookClientConfigService,
WebhookClientConfig,
Operation,
UserInfo,
WebhookFn,
WebhookServerProtocol,
)
from kopf.toolkits.hierarchies import (
adopt,
label,
Expand All @@ -138,6 +149,13 @@
append_owner_reference,
remove_owner_reference,
)
from kopf.toolkits.webhooks import (
WebhookServer,
WebhookK3dServer,
WebhookMinikubeServer,
WebhookNgrokTunnel,
WebhookInletsTunnel,
)
from kopf.utilities.piggybacking import (
login_via_pykube,
login_via_client,
Expand All @@ -158,6 +176,18 @@
'build_object_reference', 'build_owner_reference',
'append_owner_reference', 'remove_owner_reference',
'ErrorsMode',
'AdmissionError',
'WebhookClientConfigService',
'WebhookClientConfig',
'Operation',
'UserInfo',
'WebhookFn',
'WebhookServerProtocol',
'WebhookServer',
'WebhookK3dServer',
'WebhookMinikubeServer',
'WebhookNgrokTunnel',
'WebhookInletsTunnel',
'PermanentError',
'TemporaryError',
'HandlerTimeoutError',
Expand Down
33 changes: 33 additions & 0 deletions kopf/clients/creating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

from kopf.clients import auth, errors
from kopf.structs import bodies, references


@auth.reauthenticated_request
async def create_obj(
*,
resource: references.Resource,
namespace: references.Namespace = None,
name: Optional[str] = None,
body: Optional[bodies.RawBody] = None,
context: Optional[auth.APIContext] = None, # injected by the decorator
) -> Optional[bodies.RawBody]:
"""
TODO
"""
if context is None:
raise RuntimeError("API instance is not injected by the decorator.")

body = body if body is not None else {}
if namespace is not None:
body.setdefault('metadata', {}).setdefault('namespace', namespace)
if name is not None:
body.setdefault('metadata', {}).setdefault('name', name)

response = await context.session.post(
url=resource.get_url(server=context.server, namespace=namespace),
json=body,
)
created_body: bodies.RawBody = await errors.parse_response(response)
return created_body
5 changes: 5 additions & 0 deletions kopf/clients/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ class APINotFoundError(APIError):
pass


class APIConflictError(APIError):
pass


async def check_response(
response: aiohttp.ClientResponse,
) -> None:
Expand All @@ -124,6 +128,7 @@ async def check_response(
APIUnauthorizedError if response.status == 401 else
APIForbiddenError if response.status == 403 else
APINotFoundError if response.status == 404 else
APIConflictError if response.status == 409 else
APIError
)

Expand Down
Loading

0 comments on commit 0dc46bf

Please sign in to comment.