Example #1
0
def build_response(
    *,
    request: reviews.Request,
    outcomes: Mapping[ids.HandlerId, execution.Outcome],
    warnings: Collection[str],
    jsonpatch: patches.JSONPatch,
) -> reviews.Response:
    """
    Construct the admission review response to a review request.
    """
    allowed = all(outcome.exception is None
                  for id, outcome in outcomes.items())
    response = reviews.Response(
        apiVersion=request.get('apiVersion', 'admission.k8s.io/v1'),
        kind=request.get('kind', 'AdmissionReview'),
        response=reviews.ResponsePayload(uid=request.get('request',
                                                         {}).get('uid', ''),
                                         allowed=allowed))
    if warnings:
        response['response']['warnings'] = [
            str(warning) for warning in warnings
        ]
    if jsonpatch:
        encoded_patch: str = base64.b64encode(
            json.dumps(jsonpatch).encode('utf-8')).decode('ascii')
        response['response']['patch'] = encoded_patch
        response['response']['patchType'] = 'JSONPatch'

    # Prefer specialised admission errors to all other errors, Kopf's own errors to arbitrary ones.
    errors = [
        outcome.exception for outcome in outcomes.values()
        if outcome.exception is not None
    ]
    errors.sort(key=lambda error:
                (0 if isinstance(error, AdmissionError) else 1
                 if isinstance(error, execution.PermanentError) else 2
                 if isinstance(error, execution.TemporaryError) else 9))
    if errors:
        response['response']['status'] = reviews.ResponseStatus(
            message=str(errors[0]) or repr(errors[0]),
            code=(errors[0].code
                  if isinstance(errors[0], AdmissionError) else None) or 500,
        )
    return response
Example #2
0
async def serve_admission_request(
        # Required for all webhook servers, meaningless without it:
        request: reviews.Request,
        *,
        # Optional for webhook servers that can recognise this information:
        headers: Optional[Mapping[str, str]] = None,
        sslpeer: Optional[Mapping[str, Any]] = None,
        webhook: Optional[ids.HandlerId] = None,
        reason: Optional[causes.WebhookType] = None,  # TODO: undocumented: requires typing clarity!
        # Injected by partial() from spawn_tasks():
        settings: configuration.OperatorSettings,
        memories: MemoGetter,
        memobase: ephemera.AnyMemo,
        registry: registries.OperatorRegistry,
        insights: references.Insights,
        indices: ephemera.Indices,
) -> reviews.Response:
    """
    The actual and the only implementation of the `WebhookFn` protocol.

    This function is passed to all webhook servers/tunnels to be called
    whenever a new admission request is received.

    Some parameters are provided by the framework itself via partial binding,
    so that the resulting function matches the `WebhookFn` protocol. Other
    parameters are passed by the webhook servers when they call the function.
    """

    # Reconstruct the cause specially for web handlers.
    resource = find_resource(request=request, insights=insights)
    subresource = request.get('request', {}).get('subResource')
    operation = request.get('request', {}).get('operation')
    userinfo = request.get('request', {}).get('userInfo')
    new_body = request.get('request', {}).get('object')
    old_body = request.get('request', {}).get('oldObject')
    raw_body = new_body if new_body is not None else old_body
    if userinfo is None:
        raise MissingDataError("User info is missing from the admission request.")
    if raw_body is None:
        raise MissingDataError("Either old or new object is missing from the admission request.")

    memo = await memories.recall_memo(raw_body, memobase=memobase, ephemeral=operation=='CREATE')
    body = bodies.Body(raw_body)
    patch = patches.Patch(body=raw_body)
    warnings: List[str] = []
    cause = causes.WebhookCause(
        resource=resource,
        indices=indices,
        logger=loggers.LocalObjectLogger(body=body, settings=settings),
        patch=patch,
        memo=memo,
        body=body,
        userinfo=userinfo,
        warnings=warnings,
        operation=operation,
        subresource=subresource,
        dryrun=bool(request.get('request', {}).get('dryRun')),
        sslpeer=sslpeer if sslpeer is not None else {},  # ensure a mapping even if not provided.
        headers=headers if headers is not None else {},  # ensure a mapping even if not provided.
        webhook=webhook,
        reason=reason,
    )

    # Retrieve the handlers to be executed; maybe only one if the webhook server provides a hint.
    handlers_ = registry._webhooks.get_handlers(cause)
    state = progression.State.from_scratch().with_handlers(handlers_)
    outcomes = await execution.execute_handlers_once(
        lifecycle=lifecycles.all_at_once,
        settings=settings,
        handlers=handlers_,
        cause=cause,
        state=state,
        default_errors=execution.ErrorsMode.PERMANENT,
    )

    # Construct the response as per Kubernetes's conventions and expectations.
    response = build_response(
        request=request,
        outcomes=outcomes,
        warnings=warnings,
        jsonpatch=patch.as_json_patch(),
    )
    return response