示例#1
0
def test_url_for_a_list_of_clusterscoped_corev1_subresources_in_a_namespace():
    resource = Resource('', 'v1', 'plural', namespaced=False)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace='ns', subresource='status')
    assert str(err.value) in {
        "Specific namespaces are not supported for cluster-scoped resources.",
        "Subresources can be used only with specific resources by their name.",
    }
示例#2
0
async def create_obj(
    *,
    settings: configuration.OperatorSettings,
    resource: references.Resource,
    namespace: references.Namespace = None,
    name: Optional[str] = None,
    body: Optional[bodies.RawBody] = None,
    logger: typedefs.Logger,
) -> Optional[bodies.RawBody]:
    """
    Create a resource.
    """
    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)

    namespace = cast(references.Namespace,
                     body.get('metadata', {}).get('namespace'))
    created_body: bodies.RawBody = await api.post(
        url=resource.get_url(namespace=namespace),
        payload=body,
        logger=logger,
        settings=settings,
    )
    return created_body
示例#3
0
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]:
    """
    Create a resource.
    """
    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)

    namespace = cast(references.Namespace,
                     body.get('metadata', {}).get('namespace'))
    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
示例#4
0
async def watch_objs(
    *,
    settings: configuration.OperatorSettings,
    resource: references.Resource,
    namespace: references.Namespace,
    timeout: Optional[float] = None,
    since: Optional[str] = None,
    context: Optional[auth.APIContext] = None,  # injected by the decorator
    operator_pause_waiter: aiotasks.Future,
) -> AsyncIterator[bodies.RawInput]:
    """
    Watch objects of a specific resource type.

    The cluster-scoped call is used in two cases:

    * The resource itself is cluster-scoped, and namespacing makes not sense.
    * The operator serves all namespaces for the namespaced custom resource.

    Otherwise, the namespace-scoped call is used:

    * The resource is namespace-scoped AND operator is namespaced-restricted.
    """
    if context is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    params: Dict[str, str] = {}
    params['watch'] = 'true'
    if since is not None:
        params['resourceVersion'] = since
    if timeout is not None:
        params['timeoutSeconds'] = str(timeout)

    # Stream the parsed events from the response until it is closed server-side,
    # or until it is closed client-side by the pause-waiting future's callbacks.
    try:
        response = await context.session.get(
            url=resource.get_url(server=context.server,
                                 namespace=namespace,
                                 params=params),
            timeout=aiohttp.ClientTimeout(
                total=settings.watching.client_timeout,
                sock_connect=settings.watching.connect_timeout,
            ),
        )
        await errors.check_response(response)

        response_close_callback = lambda _: response.close()
        operator_pause_waiter.add_done_callback(response_close_callback)
        try:
            async with response:
                async for line in _iter_jsonlines(response.content):
                    raw_input = cast(bodies.RawInput,
                                     json.loads(line.decode("utf-8")))
                    yield raw_input
        finally:
            operator_pause_waiter.remove_done_callback(response_close_callback)

    except (aiohttp.ClientConnectionError, aiohttp.ClientPayloadError,
            asyncio.TimeoutError):
        pass
示例#5
0
async def watch_objs(
    *,
    settings: configuration.OperatorSettings,
    resource: references.Resource,
    namespace: references.Namespace,
    since: Optional[str] = None,
    operator_pause_waiter: aiotasks.Future,
) -> AsyncIterator[bodies.RawInput]:
    """
    Watch objects of a specific resource type.

    The cluster-scoped call is used in two cases:

    * The resource itself is cluster-scoped, and namespacing makes not sense.
    * The operator serves all namespaces for the namespaced custom resource.

    Otherwise, the namespace-scoped call is used:

    * The resource is namespace-scoped AND operator is namespaced-restricted.
    """
    params: Dict[str, str] = {}
    params['watch'] = 'true'
    if since is not None:
        params['resourceVersion'] = since
    if settings.watching.server_timeout is not None:
        params['timeoutSeconds'] = str(settings.watching.server_timeout)

    connect_timeout = (settings.watching.connect_timeout
                       if settings.watching.connect_timeout is not None else
                       settings.networking.connect_timeout
                       if settings.networking.connect_timeout is not None else
                       settings.networking.request_timeout)

    # Stream the parsed events from the response until it is closed server-side,
    # or until it is closed client-side by the pause-waiting future's callbacks.
    try:
        async for raw_input in api.stream(
                url=resource.get_url(namespace=namespace, params=params),
                logger=logger,
                settings=settings,
                stopper=operator_pause_waiter,
                timeout=aiohttp.ClientTimeout(
                    total=settings.watching.client_timeout,
                    sock_connect=connect_timeout,
                ),
        ):
            yield raw_input

    except (aiohttp.ClientConnectionError, aiohttp.ClientPayloadError,
            asyncio.TimeoutError):
        pass
示例#6
0
async def list_objs(
    *,
    settings: configuration.OperatorSettings,
    resource: references.Resource,
    namespace: references.Namespace,
    logger: Union[logging.Logger, logging.LoggerAdapter],
) -> Tuple[Collection[bodies.RawBody], str]:
    """
    List the objects of specific resource type.

    The cluster-scoped call is used in two cases:

    * The resource itself is cluster-scoped, and namespacing makes not sense.
    * The operator serves all namespaces for the namespaced custom resource.

    Otherwise, the namespace-scoped call is used:

    * The resource is namespace-scoped AND operator is namespaced-restricted.
    """
    rsp = await api.get(
        url=resource.get_url(namespace=namespace),
        logger=logger,
        settings=settings,
    )

    items: List[bodies.RawBody] = []
    resource_version = rsp.get('metadata', {}).get('resourceVersion', None)
    for item in rsp.get('items', []):
        if 'kind' in rsp:
            item.setdefault(
                'kind', rsp['kind'][:-4]
                if rsp['kind'][-4:] == 'List' else rsp['kind'])
        if 'apiVersion' in rsp:
            item.setdefault('apiVersion', rsp['apiVersion'])
        items.append(item)

    return items, resource_version
示例#7
0
async def list_objs_rv(
        *,
        resource: references.Resource,
        namespace: references.Namespace,
        context: Optional[auth.APIContext] = None,  # injected by the decorator
) -> Tuple[Collection[bodies.RawBody], str]:
    """
    List the objects of specific resource type.

    The cluster-scoped call is used in two cases:

    * The resource itself is cluster-scoped, and namespacing makes not sense.
    * The operator serves all namespaces for the namespaced custom resource.

    Otherwise, the namespace-scoped call is used:

    * The resource is namespace-scoped AND operator is namespaced-restricted.
    """
    if context is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    url = resource.get_url(server=context.server, namespace=namespace)
    rsp = await errors.parse_response(await context.session.get(url))

    items: List[bodies.RawBody] = []
    resource_version = rsp.get('metadata', {}).get('resourceVersion', None)
    for item in rsp['items']:
        if 'kind' in rsp:
            item.setdefault(
                'kind', rsp['kind'][:-4]
                if rsp['kind'][-4:] == 'List' else rsp['kind'])
        if 'apiVersion' in rsp:
            item.setdefault('apiVersion', rsp['apiVersion'])
        items.append(item)

    return items, resource_version
示例#8
0
def test_url_for_a_list_of_clusterscoped_corev1_resources_in_a_namespace():
    resource = Resource('', 'v1', 'plural', namespaced=False)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace='ns')
    assert str(err.value) == "Specific namespaces are not supported for cluster-scoped resources."
示例#9
0
def test_url_for_a_list_of_clusterscoped_corev1_resources_clusterwide():
    resource = Resource('', 'v1', 'plural', namespaced=False)
    url = resource.get_url(namespace=None)
    assert url == '/api/v1/plural'
示例#10
0
def test_url_for_a_specific_namespaced_custom_resource_in_a_namespace():
    resource = Resource('group', 'version', 'plural', namespaced=True)
    url = resource.get_url(namespace='ns-a.b', name='name-a.b')
    assert url == '/apis/group/version/namespaces/ns-a.b/plural/name-a.b'
示例#11
0
def test_url_for_a_specific_namespaced_custom_resource_clusterwide():
    resource = Resource('group', 'version', 'plural', namespaced=True)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace=None, name='name-a.b')
    assert str(err.value) == "Specific namespaces are required for specific namespaced resources."
示例#12
0
def test_url_for_a_specific_clusterscoped_custom_resource_in_a_namespace():
    resource = Resource('group', 'version', 'plural', namespaced=False)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace='ns', name='name-a.b')
    assert str(err.value) == "Specific namespaces are not supported for cluster-scoped resources."
示例#13
0
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'
示例#14
0
def test_url_for_a_list_of_namespaced_custom_subresources_clusterwide():
    resource = Resource('group', 'version', 'plural', namespaced=True)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace=None, subresource='status')
    assert str(err.value) == "Subresources can be used only with specific resources by their name."
示例#15
0
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'
示例#16
0
def test_url_for_a_list_of_namespaced_corev1_subresources_in_a_namespace():
    resource = Resource('', 'v1', 'plural', namespaced=True)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace='ns-a.b', subresource='status')
    assert str(err.value) == "Subresources can be used only with specific resources by their name."
示例#17
0
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'
示例#18
0
def test_url_for_a_list_of_clusterscoped_corev1_subresources_clusterwide():
    resource = Resource('', 'v1', 'plural', namespaced=False)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace=None, subresource='status')
    assert str(err.value) == "Subresources can be used only with specific resources by their name."
示例#19
0
async def post_event(
    *,
    ref: bodies.ObjectReference,
    type: str,
    reason: str,
    message: str = '',
    resource: references.Resource,
    settings: configuration.OperatorSettings,
    logger: typedefs.Logger,
) -> None:
    """
    Issue an event for the object.

    This is where they can also be accumulated, aggregated, grouped,
    and where the rate-limits should be maintained. It can (and should)
    be done by the client library, as it is done in the Go client.
    """

    # Prevent "event explosion", when core v1 events are handled and create other core v1 events.
    # This can happen with `EVERYTHING` without additional filters, or by explicitly serving them.
    if ref['apiVersion'] == 'v1' and ref['kind'] == 'Event':
        return

    # See #164. For cluster-scoped objects, use the current namespace from the current context.
    # It could be "default", but in some systems, we are limited to one specific namespace only.
    namespace_name: str = ref.get('namespace') or (
        await api.get_default_namespace()) or 'default'
    namespace = references.NamespaceName(namespace_name)
    full_ref: bodies.ObjectReference = copy.copy(ref)
    full_ref['namespace'] = namespace

    # Prevent a common case of event posting errors but shortening the message.
    if len(message) > MAX_MESSAGE_LENGTH:
        infix = CUT_MESSAGE_INFIX
        prefix = message[:MAX_MESSAGE_LENGTH // 2 - (len(infix) // 2)]
        suffix = message[-MAX_MESSAGE_LENGTH // 2 +
                         (len(infix) - len(infix) // 2):]
        message = f'{prefix}{infix}{suffix}'

    now = datetime.datetime.utcnow()
    body = {
        'metadata': {
            'namespace': namespace,
            'generateName': 'kopf-event-',
        },
        'action': 'Action?',
        'type': type,
        'reason': reason,
        'message': message,
        'reportingComponent': 'kopf',
        'reportingInstance': 'dev',
        'source': {
            'component': 'kopf'
        },  # used in the "From" column in `kubectl describe`.
        'involvedObject': full_ref,
        'firstTimestamp': now.isoformat() +
        'Z',  # '2019-01-28T18:25:03.000000Z' -- seen in `kubectl describe ...`
        'lastTimestamp': now.isoformat() +
        'Z',  # '2019-01-28T18:25:03.000000Z' - seen in `kubectl get events`
        'eventTime': now.isoformat() + 'Z',  # '2019-01-28T18:25:03.000000Z'
    }

    try:
        await api.post(
            url=resource.get_url(namespace=namespace),
            headers={'Content-Type': 'application/json'},
            payload=body,
            logger=logger,
            settings=settings,
        )

    # Events are helpful but auxiliary, they should not fail the handling cycle.
    # Yet we want to notice that something went wrong (in logs).
    except errors.APIError as e:
        logger.warning(
            f"Failed to post an event. Ignoring and continuing. "
            f"Code: {e.code}. Message: {e.message}. Details: {e.details}"
            f"Event: type={type!r}, reason={reason!r}, message={message!r}.")
    except aiohttp.ClientResponseError as e:
        logger.warning(
            f"Failed to post an event. Ignoring and continuing. "
            f"Status: {e.status}. Message: {e.message}. "
            f"Event: type={type!r}, reason={reason!r}, message={message!r}.")
    except aiohttp.ServerDisconnectedError as e:
        logger.warning(
            f"Failed to post an event. Ignoring and continuing. "
            f"Message: {e.message}. "
            f"Event: type={type!r}, reason={reason!r}, message={message!r}.")
    except aiohttp.ClientOSError:
        logger.warning(
            f"Failed to post an event. Ignoring and continuing. "
            f"Event: type={type!r}, reason={reason!r}, message={message!r}.")
示例#20
0
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'
示例#21
0
def test_url_for_a_specific_namespaced_corev1_subresource_clusterwide():
    resource = Resource('', 'v1', 'plural', namespaced=True)
    with pytest.raises(ValueError) as err:
        resource.get_url(namespace=None, name='name-a.b', subresource='status')
    assert str(err.value) == "Specific namespaces are required for specific namespaced resources."
示例#22
0
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'
示例#23
0
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
示例#24
0
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'